import { Html } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import { Component, Suspense, useEffect, useMemo, useRef, useState } from "react";
import type { CSSProperties, ErrorInfo, ReactNode } from "react";
import {
  AdditiveBlending,
  AnimationMixer,
  DoubleSide,
  LoopOnce,
  LoopRepeat,
  NormalBlending,
  type AnimationAction,
  type Group,
  type Material,
  type Mesh,
  type Object3D,
  Vector3,
} from "three";
import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js";
import {
  createMobAnimationTransform,
  getMobAnimationPhase,
  resolveMobAnimationClip,
  resolveMobCombatAnimationClip,
  resolveMobAnimationState,
  sampleMobCombatFallback,
  sampleMobAnimation,
} from "../game/mobAnimation";
import type { MobAnimationState } from "../game/mobAnimation";
import { useCombatStore } from "../game/combatStore";
import { PRIMARY_MOUSE_BUTTON } from "../game/inputManager";
import { TimedEffectStrip } from "../ui/TimedEffectStrip";
import { AuraStrip } from "../ui/AuraStrip";
import { bossHasLoot } from "../game/lootCatalog";
import { usePartyStore } from "../game/partyStore";
import {
  getPartyRuntimePosition,
  partyRuntimeDistanceSquared,
} from "../game/partyRuntimeRegistry";
import { PLAYER_AGGRO_ID } from "../game/aggro";
import {
  registerMobRuntime,
  updateMobRuntimePosition,
} from "../game/mobRuntimeRegistry";
import { registerDungeonRuntimeReset } from "../game/dungeonRuntimeReset";
import { scaledDungeonEnemyLevel } from "../game/dungeonLevelScaling";
import { stepFearedMobAway } from "../game/mobMotion";
import {
  clusterStaticMobSpawns,
  createLoopedPath,
  samplePackFormation,
} from "../game/mobPopulation";
import { useGameStore } from "../game/store";
import {
  manastormExperienceReward,
  manastormRuntimeScaling,
} from "../game/manastorm";
import { useManastormStore } from "../game/manastormStore";
import type {
  MobArchetype,
  MobModelStyle,
  MobPackDefinition,
  MobVector3,
  MutableMobVector3,
  PopulationDefinitionMap,
  PopulationEntityDefinition,
  StaticMobSpawnDefinition,
} from "../game/mobPopulation";
import { disposeObject3DResources } from "./threeResourceDisposal";
import {
  advanceMobAnimationCadence,
  createMobAnimationCadenceState,
  resolveMobAnimationInterval,
} from "./mobAnimationCadence";
import { useGameGLTF } from "./useGameGLTF";

const MOVEMENT_LOCKING_MOB_STATUSES: ReadonlySet<string> = new Set([
  "stun", "root", "sleep", "knockdown", "freeze", "charm", "confuse",
]);

const mobLabelStyle: CSSProperties = {
  color: "#eaf4dd",
  fontFamily: "system-ui, sans-serif",
  fontSize: "11px",
  fontWeight: 700,
  lineHeight: 1.1,
  pointerEvents: "none",
  textAlign: "center",
  textShadow: "0 1px 2px #000, 0 0 5px #000",
  userSelect: "none",
  whiteSpace: "nowrap",
};

const bossLabelStyle: CSSProperties = {
  ...mobLabelStyle,
  color: "#ffe69a",
  fontSize: "13px",
};

const lootLabelStyle: CSSProperties = {
  ...bossLabelStyle,
  padding: "5px 9px",
  border: "1px solid rgba(245, 212, 112, 0.72)",
  borderRadius: "999px",
  background: "rgba(14, 12, 8, 0.88)",
  boxShadow: "0 4px 18px rgba(0, 0, 0, 0.55)",
};

const POPULATION_ACTIVATION_DISTANCE = 95;

function mobTargetPosition(
  targetActorId: string | null | undefined,
  playerPosition: MobVector3,
): MobVector3 {
  if (!targetActorId || targetActorId === PLAYER_AGGRO_ID) return playerPosition;
  return getPartyRuntimePosition(targetActorId) ?? playerPosition;
}
const PROXIMITY_CHECK_INTERVAL = 0.3;
let preparedCreatureTemplates = new WeakMap<Object3D, Object3D>();
let preparedCreatureTemplateRoots = new Set<Object3D>();
registerDungeonRuntimeReset(() => {
  const releasedTemplates = preparedCreatureTemplateRoots;
  preparedCreatureTemplates = new WeakMap<Object3D, Object3D>();
  preparedCreatureTemplateRoots = new Set<Object3D>();
  // Dungeon switches publish the next scene in the same event turn. Defer GPU
  // disposal until React has removed every old creature clone from the Canvas.
  queueMicrotask(() => {
    for (const template of releasedTemplates) disposeObject3DResources(template);
  });
});

interface ProxyMobProps {
  readonly definition: PopulationEntityDefinition;
  readonly instanceId: string;
  readonly active: boolean;
  readonly moving: boolean;
  readonly showLabel: boolean;
  readonly useOriginalModels: boolean;
}

interface CreatureModelErrorBoundaryProps {
  readonly children: ReactNode;
  readonly fallback: ReactNode;
  readonly resetKey: string;
}

class CreatureModelErrorBoundary extends Component<
  CreatureModelErrorBoundaryProps,
  { readonly failed: boolean }
> {
  state = { failed: false };

  static getDerivedStateFromError() {
    return { failed: true };
  }

  componentDidCatch(error: Error, info: ErrorInfo): void {
    console.warn("Creature model fell back to procedural rendering", error, info.componentStack);
  }

  componentDidUpdate(previous: Readonly<CreatureModelErrorBoundaryProps>): void {
    if (previous.resetKey !== this.props.resetKey && this.state.failed) {
      this.setState({ failed: false });
    }
  }

  render(): ReactNode {
    return this.state.failed ? this.props.fallback : this.props.children;
  }
}

function prepareCreatureModel(root: Object3D): Object3D {
  const cachedTemplate = preparedCreatureTemplates.get(root);
  if (cachedTemplate) return cachedTemplate;

  const clone = cloneSkeleton(root);
  const materials = new Map<string, Material>();

  clone.traverse((node) => {
    const mesh = node as Mesh;
    if (!mesh.isMesh) return;

    const sourceMaterials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
    const preparedMaterials = sourceMaterials.map((source) => {
      const cached = materials.get(source.uuid);
      if (cached) return cached;

      const material = source.clone();
      material.side = DoubleSide;
      if (/_B1$/i.test(material.name)) {
        material.transparent = false;
        material.opacity = 1;
        material.alphaTest = 0.5;
        material.depthWrite = true;
        material.blending = NormalBlending;
      } else if (/_B2$/i.test(material.name)) {
        material.transparent = true;
        material.depthWrite = false;
        material.blending = NormalBlending;
      } else if (/_B4$/i.test(material.name)) {
        material.transparent = true;
        material.depthWrite = false;
        material.blending = AdditiveBlending;
      } else {
        material.transparent = false;
        material.opacity = 1;
        material.alphaTest = 0;
        material.depthWrite = true;
        material.blending = NormalBlending;
      }
      material.needsUpdate = true;
      materials.set(source.uuid, material);
      return material;
    });

    mesh.material = Array.isArray(mesh.material) ? preparedMaterials : preparedMaterials[0];
    // Creature shadows were a large extra pass across actors that are usually
    // hidden by cave bends. The dungeon lighting and emissive encounter markers
    // preserve silhouettes without paying that cost.
    mesh.castShadow = false;
    mesh.receiveShadow = false;
  });

  preparedCreatureTemplates.set(root, clone);
  preparedCreatureTemplateRoots.add(clone);
  return clone;
}

function scheduledMobAnimationDelta(
  object: Object3D,
  camera: Object3D,
  worldPosition: Vector3,
  cadenceState: ReturnType<typeof createMobAnimationCadenceState>,
  delta: number,
  active: boolean,
  forceFullFidelity: boolean,
): number {
  object.getWorldPosition(worldPosition);
  // A Three camera looks down its local -Z axis. Reading matrixWorld avoids a
  // second temporary vector and does not allocate inside the render loop.
  const matrix = camera.matrixWorld.elements;
  const dx = worldPosition.x - matrix[12];
  const dy = worldPosition.y - matrix[13];
  const dz = worldPosition.z - matrix[14];
  const distanceSquared = dx * dx + dy * dy + dz * dz;
  const inFrontOfCamera = dx * -matrix[8] + dy * -matrix[9] + dz * -matrix[10] >= 0;
  const interval = resolveMobAnimationInterval(
    active,
    forceFullFidelity,
    distanceSquared,
    inFrontOfCamera,
  );
  return advanceMobAnimationCadence(cadenceState, delta, interval);
}

function OriginalCreatureModel({
  model,
  active,
  proceduralAnimation,
  animationState,
  attackRevision,
  attackAnimation,
  woundRevision,
  forceFullFidelity,
}: {
  readonly model: MobModelStyle;
  readonly active: boolean;
  readonly proceduralAnimation: boolean;
  readonly animationState: MobAnimationState;
  readonly attackRevision: number;
  readonly attackAnimation: "attack" | "cast";
  readonly woundRevision: number;
  readonly forceFullFidelity: boolean;
}) {
  const gltf = useGameGLTF(model.url);
  const template = useMemo(() => prepareCreatureModel(gltf.scene), [gltf.scene]);
  // SkeletonUtils preserves independent bone bindings when animated creature
  // exports are present, while still sharing immutable geometry and materials.
  const scene = useMemo(() => cloneSkeleton(template), [template]);
  const mixer = useMemo(() => new AnimationMixer(scene), [scene]);
  const baseActionRef = useRef<AnimationAction | null>(null);
  const combatActionRef = useRef<AnimationAction | null>(null);
  const observedAttackRevisionRef = useRef(attackRevision);
  const observedWoundRevisionRef = useRef(woundRevision);
  const fallbackObservedAttackRevisionRef = useRef(attackRevision);
  const fallbackObservedWoundRevisionRef = useRef(woundRevision);
  const fallbackAttackElapsedRef = useRef(1);
  const fallbackWoundElapsedRef = useRef(1);
  const fallbackPoseRef = useRef<Group>(null);
  const stateElapsedRef = useRef(0);
  const previousStateRef = useRef(animationState);
  const terminalCompleteRef = useRef(false);
  const fallbackTransform = useMemo(() => createMobAnimationTransform(), []);
  const cadenceState = useMemo(() => createMobAnimationCadenceState(), []);
  const worldPosition = useMemo(() => new Vector3(), []);
  const resolution = useMemo(
    () => proceduralAnimation ? null : resolveMobAnimationClip(gltf.animations, animationState),
    [animationState, gltf.animations, proceduralAnimation],
  );
  const combatClipAvailability = useMemo(() => ({
    attack: resolveMobCombatAnimationClip(gltf.animations, "attack", 0) !== null,
    cast: resolveMobCombatAnimationClip(gltf.animations, "cast", 0) !== null,
    wound: resolveMobCombatAnimationClip(gltf.animations, "wound", 0) !== null,
  }), [gltf.animations]);

  useFrame(({ camera }, delta) => {
    if (previousStateRef.current !== animationState) {
      previousStateRef.current = animationState;
      stateElapsedRef.current = 0;
      terminalCompleteRef.current = false;
    }
    if (fallbackObservedAttackRevisionRef.current !== attackRevision) {
      fallbackObservedAttackRevisionRef.current = attackRevision;
      fallbackAttackElapsedRef.current = 0;
    }
    if (fallbackObservedWoundRevisionRef.current !== woundRevision) {
      fallbackObservedWoundRevisionRef.current = woundRevision;
      fallbackWoundElapsedRef.current = 0;
    }
    if (animationState === "dead" && terminalCompleteRef.current) return;

    const safeDelta = scheduledMobAnimationDelta(
      scene,
      camera,
      worldPosition,
      cadenceState,
      delta,
      active,
      forceFullFidelity,
    );
    if (safeDelta <= 0) return;
    stateElapsedRef.current += safeDelta;
    fallbackAttackElapsedRef.current += safeDelta;
    fallbackWoundElapsedRef.current += safeDelta;
    const fallbackPose = fallbackPoseRef.current;
    if (fallbackPose) {
      const missingDeathClip = animationState === "dead" && !resolution?.exact;
      const missingWoundClip = fallbackWoundElapsedRef.current < 0.36
        && !combatClipAvailability.wound;
      const missingAttackClip = fallbackAttackElapsedRef.current < 0.8
        && !combatClipAvailability[attackAnimation];
      if (!proceduralAnimation && (missingDeathClip || missingWoundClip || missingAttackClip)) {
        const fallbackState = missingDeathClip
          ? "dead"
          : missingWoundClip
            ? "wound"
            : "attacking";
        const elapsed = missingDeathClip
          ? stateElapsedRef.current
          : missingWoundClip
            ? fallbackWoundElapsedRef.current
            : fallbackAttackElapsedRef.current;
        sampleMobCombatFallback(fallbackState, elapsed, fallbackTransform);
        fallbackPose.position.set(0, fallbackTransform.offsetY, fallbackTransform.offsetZ);
        fallbackPose.rotation.set(
          fallbackTransform.rotationX,
          fallbackTransform.rotationY,
          fallbackTransform.rotationZ,
        );
        fallbackPose.scale.set(
          fallbackTransform.scaleX,
          fallbackTransform.scaleY,
          fallbackTransform.scaleZ,
        );
      } else {
        fallbackPose.position.set(0, 0, 0);
        fallbackPose.rotation.set(0, 0, 0);
        fallbackPose.scale.set(1, 1, 1);
      }
    }

    // Do not evaluate hundreds of bone tracks while paused or behind the map.
    if (!proceduralAnimation) mixer.update(safeDelta);
  });

  useEffect(() => {
    const clip = resolution?.clip;
    if (!clip) return;
    terminalCompleteRef.current = false;
    stateElapsedRef.current = 0;
    const action = mixer.clipAction(clip, scene);
    const terminal = animationState === "dead";

    if (terminal) {
      combatActionRef.current?.fadeOut(0.08);
      combatActionRef.current = null;
    }
    action.clampWhenFinished = terminal;
    action.setLoop(terminal ? LoopOnce : LoopRepeat, terminal ? 1 : Number.POSITIVE_INFINITY);
    const handleFinished = () => {
      if (terminal) terminalCompleteRef.current = true;
    };
    if (terminal) mixer.addEventListener("finished", handleFinished);

    action.reset().fadeIn(0.15).play();
    baseActionRef.current = action;
    return () => {
      if (terminal) mixer.removeEventListener("finished", handleFinished);
      action.fadeOut(0.15);
      if (baseActionRef.current === action) baseActionRef.current = null;
    };
  }, [animationState, mixer, resolution, scene]);

  const playCombatAction = (kind: "attack" | "cast" | "wound", revision: number) => {
    if (animationState === "dead") return;
    const clip = resolveMobCombatAnimationClip(gltf.animations, kind, revision);
    if (!clip) return;
    const action = mixer.clipAction(clip, scene);
    combatActionRef.current?.fadeOut(0.08);
    baseActionRef.current?.fadeOut(0.08);
    action.enabled = true;
    action.clampWhenFinished = false;
    action.setLoop(LoopOnce, 1).reset().fadeIn(0.08).play();
    combatActionRef.current = action;
  };

  useEffect(() => {
    if (attackRevision === observedAttackRevisionRef.current) return;
    observedAttackRevisionRef.current = attackRevision;
    playCombatAction(attackAnimation, attackRevision);
  // The model, mixer, and scene remain stable for this mounted creature.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [attackAnimation, attackRevision]);

  useEffect(() => {
    if (woundRevision === observedWoundRevisionRef.current) return;
    observedWoundRevisionRef.current = woundRevision;
    playCombatAction("wound", woundRevision);
  // The model, mixer, and scene remain stable for this mounted creature.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [woundRevision]);

  useEffect(() => {
    const handleFinished = (event: { action: AnimationAction }) => {
      if (event.action !== combatActionRef.current) return;
      combatActionRef.current = null;
      event.action.fadeOut(0.1);
      baseActionRef.current?.reset().fadeIn(0.1).play();
    };
    mixer.addEventListener("finished", handleFinished);
    return () => mixer.removeEventListener("finished", handleFinished);
  }, [mixer]);

  useEffect(() => () => {
    mixer.stopAllAction();
    mixer.uncacheRoot(scene);
    disposeObject3DResources(scene, {
      geometries: false,
      materials: false,
      textures: false,
    });
    baseActionRef.current = null;
    combatActionRef.current = null;
  }, [mixer, scene]);

  return (
    <group ref={fallbackPoseRef}>
      <primitive
        object={scene}
        position={[0, model.groundOffset, 0]}
        rotation={[0, model.rotationY, 0]}
      />
    </group>
  );
}

function AnimatedMobBody({
  active,
  archetype,
  animationState,
  attackRevision,
  woundRevision,
  instanceId,
  motionWeight,
  proceduralCombatFallback,
  forceFullFidelity,
  children,
}: {
  readonly active: boolean;
  readonly archetype: MobArchetype;
  readonly animationState: MobAnimationState;
  readonly attackRevision: number;
  readonly woundRevision: number;
  readonly instanceId: string;
  readonly motionWeight: number;
  readonly proceduralCombatFallback: boolean;
  readonly forceFullFidelity: boolean;
  readonly children: ReactNode;
}) {
  const bodyRef = useRef<Group>(null);
  const elapsedRef = useRef(0);
  const previousStateRef = useRef(animationState);
  const combatPoseSettledRef = useRef(false);
  const observedAttackRevisionRef = useRef(attackRevision);
  const attackElapsedRef = useRef(1);
  const observedWoundRevisionRef = useRef(woundRevision);
  const woundElapsedRef = useRef(1);
  const phase = useMemo(() => getMobAnimationPhase(instanceId), [instanceId]);
  const transform = useMemo(() => createMobAnimationTransform(), []);
  const cadenceState = useMemo(() => createMobAnimationCadenceState(), []);
  const worldPosition = useMemo(() => new Vector3(), []);

  useFrame(({ camera }, delta) => {
    const body = bodyRef.current;
    if (!body) return;

    if (previousStateRef.current !== animationState) {
      previousStateRef.current = animationState;
      elapsedRef.current = 0;
      combatPoseSettledRef.current = false;
    }
    if (observedAttackRevisionRef.current !== attackRevision) {
      observedAttackRevisionRef.current = attackRevision;
      attackElapsedRef.current = 0;
    }
    if (observedWoundRevisionRef.current !== woundRevision) {
      observedWoundRevisionRef.current = woundRevision;
      woundElapsedRef.current = 0;
    }

    if (animationState === "dead" && combatPoseSettledRef.current) return;

    const safeDelta = scheduledMobAnimationDelta(
      body,
      camera,
      worldPosition,
      cadenceState,
      delta,
      active,
      forceFullFidelity,
    );
    if (safeDelta <= 0) return;
    elapsedRef.current += safeDelta;
    attackElapsedRef.current += safeDelta;
    woundElapsedRef.current += safeDelta;
    if (animationState === "dead") {
      if (proceduralCombatFallback) {
        sampleMobCombatFallback(animationState, elapsedRef.current, transform);
        // The procedural death pose clamps at 0.72 seconds. Stop touching the
        // corpse transform after applying that terminal sample once.
        if (animationState === "dead" && elapsedRef.current >= 0.72) {
          combatPoseSettledRef.current = true;
        }
      } else {
        transform.offsetY = 0;
        transform.offsetZ = 0;
        transform.rotationX = 0;
        transform.rotationY = 0;
        transform.rotationZ = 0;
        transform.scaleX = 1;
        transform.scaleY = 1;
        transform.scaleZ = 1;
        // Extracted model clips animate inside this wrapper. Reset the prior
        // locomotion root pose once, then leave subsequent frames to its mixer.
        combatPoseSettledRef.current = true;
      }
    } else if (proceduralCombatFallback && woundElapsedRef.current < 0.36) {
      sampleMobCombatFallback("wound", woundElapsedRef.current, transform);
    } else if (proceduralCombatFallback && attackElapsedRef.current < 0.8) {
      sampleMobCombatFallback("attacking", attackElapsedRef.current, transform);
    } else {
      sampleMobAnimation(archetype, animationState === "moving", elapsedRef.current, phase, transform);
    }
    body.position.y = transform.offsetY * motionWeight;
    body.position.z = transform.offsetZ * motionWeight;
    body.rotation.set(
      transform.rotationX * motionWeight,
      transform.rotationY * motionWeight,
      transform.rotationZ * motionWeight,
    );
    body.scale.set(
      1 + (transform.scaleX - 1) * motionWeight,
      1 + (transform.scaleY - 1) * motionWeight,
      1 + (transform.scaleZ - 1) * motionWeight,
    );
  });

  return <group ref={bodyRef}>{children}</group>;
}

function ProxyBody({
  archetype,
  primaryColor,
  accentColor,
}: {
  readonly archetype: MobArchetype;
  readonly primaryColor: string;
  readonly accentColor: string;
}) {
  const primary = <meshStandardMaterial color={primaryColor} roughness={0.84} />;
  const accent = <meshStandardMaterial color={accentColor} roughness={0.72} />;

  switch (archetype) {
    case "raptor":
      return (
        <>
          <mesh position={[0, 0.9, 0]} scale={[0.75, 0.58, 1.2]}>
            <dodecahedronGeometry args={[0.7, 0]} />{primary}
          </mesh>
          <mesh position={[0, 1.12, 0.88]} scale={[0.7, 0.62, 0.9]}>
            <icosahedronGeometry args={[0.44, 0]} />{accent}
          </mesh>
          <mesh position={[0, 0.83, -1.05]} rotation={[-Math.PI / 2, 0, 0]}>
            <coneGeometry args={[0.32, 1.5, 6]} />{primary}
          </mesh>
          {[-0.38, 0.38].map((x) => (
            <mesh key={x} position={[x, 0.34, 0.12]}>
              <cylinderGeometry args={[0.12, 0.16, 0.65, 5]} />{primary}
            </mesh>
          ))}
        </>
      );
    case "crocolisk":
      return (
        <>
          <mesh position={[0, 0.5, 0]} scale={[0.72, 0.4, 1.55]}>
            <dodecahedronGeometry args={[0.72, 0]} />{primary}
          </mesh>
          <mesh position={[0, 0.48, 1.16]} scale={[0.72, 0.42, 0.9]}>
            <boxGeometry args={[0.9, 0.55, 0.9]} />{accent}
          </mesh>
          <mesh position={[0, 0.48, -1.35]} rotation={[-Math.PI / 2, 0, 0]}>
            <coneGeometry args={[0.38, 1.65, 6]} />{primary}
          </mesh>
          {[-0.5, 0.5].flatMap((x) => [-0.45, 0.45].map((z) => (
            <mesh key={`${x}-${z}`} position={[x, 0.18, z]} scale={[1, 0.55, 1]}>
              <sphereGeometry args={[0.2, 6, 4]} />{primary}
            </mesh>
          )))}
        </>
      );
    case "ooze":
      return (
        <>
          <mesh position={[0, 0.62, 0]} scale={[1, 0.82, 1]}>
            <dodecahedronGeometry args={[0.78, 1]} />
            <meshStandardMaterial color={primaryColor} emissive={accentColor} emissiveIntensity={0.18} roughness={0.45} />
          </mesh>
          <mesh position={[-0.25, 0.82, 0.58]}><sphereGeometry args={[0.09, 8, 6]} />{accent}</mesh>
          <mesh position={[0.25, 0.82, 0.58]}><sphereGeometry args={[0.09, 8, 6]} />{accent}</mesh>
        </>
      );
    case "plant":
      return (
        <>
          <mesh position={[0, 0.8, 0]}>
            <cylinderGeometry args={[0.35, 0.52, 1.5, 7]} />{primary}
          </mesh>
          <mesh position={[0, 1.65, 0]} scale={[0.9, 0.72, 0.9]}>
            <dodecahedronGeometry args={[0.68, 0]} />{accent}
          </mesh>
          {[-1, 1].map((side) => (
            <mesh key={side} position={[side * 0.58, 1.05, 0]} rotation={[0, 0, side * 0.75]}>
              <coneGeometry args={[0.18, 1.1, 5]} />{primary}
            </mesh>
          ))}
        </>
      );
    case "serpent":
      return (
        <>
          {[0, 0.48, 0.92].map((z, index) => (
            <mesh key={z} position={[0, 0.38 + index * 0.16, z - 0.55]} scale={[1, 0.82, 1.15]}>
              <sphereGeometry args={[0.42 - index * 0.045, 8, 6]} />{primary}
            </mesh>
          ))}
          <mesh position={[0, 1.08, 0.92]} scale={[0.88, 0.72, 1]}>
            <icosahedronGeometry args={[0.48, 0]} />{accent}
          </mesh>
        </>
      );
    case "turtle":
      return (
        <>
          <mesh position={[0, 0.58, 0]} scale={[1.18, 0.58, 1.42]}>
            <dodecahedronGeometry args={[0.72, 1]} />{primary}
          </mesh>
          <mesh position={[0, 0.72, 0]} scale={[1.05, 0.42, 1.27]}>
            <sphereGeometry args={[0.72, 10, 6]} />{accent}
          </mesh>
          <mesh position={[0, 0.52, 1.14]}>
            <icosahedronGeometry args={[0.34, 0]} />{primary}
          </mesh>
        </>
      );
    case "lizard":
      return (
        <>
          <mesh position={[0, 0.72, 0]} scale={[0.88, 0.64, 1.42]}>
            <dodecahedronGeometry args={[0.75, 0]} />{primary}
          </mesh>
          <mesh position={[0, 0.88, 1.1]} scale={[0.8, 0.72, 0.95]}>
            <icosahedronGeometry args={[0.5, 0]} />{accent}
          </mesh>
          <mesh position={[0, 0.63, -1.32]} rotation={[-Math.PI / 2, 0, 0]}>
            <coneGeometry args={[0.38, 1.8, 6]} />{primary}
          </mesh>
          {[-0.38, 0, 0.38].map((z) => (
            <mesh key={z} position={[0, 1.25, z]}>
              <coneGeometry args={[0.12, 0.42, 4]} />{accent}
            </mesh>
          ))}
        </>
      );
    case "murloc":
      return (
        <>
          <mesh position={[0, 0.95, 0]} scale={[1, 1.08, 0.72]}>
            <dodecahedronGeometry args={[0.68, 0]} />{primary}
          </mesh>
          <mesh position={[0, 1.55, 0.12]} scale={[1.05, 0.78, 0.72]}>
            <icosahedronGeometry args={[0.5, 1]} />{accent}
          </mesh>
          {[-0.24, 0.24].map((x) => (
            <mesh key={x} position={[x, 1.72, 0.47]}>
              <sphereGeometry args={[0.11, 8, 6]} />
              <meshStandardMaterial color="#f4f2bc" emissive="#d8e88b" emissiveIntensity={0.4} />
            </mesh>
          ))}
        </>
      );
    case "winged":
      return (
        <>
          <mesh position={[0, 1.05, 0]} scale={[0.68, 0.88, 0.9]}>
            <icosahedronGeometry args={[0.62, 1]} />{primary}
          </mesh>
          <mesh position={[0, 1.62, 0.28]}><sphereGeometry args={[0.34, 10, 7]} />{accent}</mesh>
          {[-1, 1].map((side) => (
            <mesh key={side} position={[side * 0.72, 1.17, -0.08]} rotation={[0, 0, side * 0.72]} scale={[0.45, 1.2, 0.22]}>
              <dodecahedronGeometry args={[0.55, 0]} />
              <meshStandardMaterial color={accentColor} emissive={accentColor} emissiveIntensity={0.22} roughness={0.58} />
            </mesh>
          ))}
        </>
      );
    case "humanoid":
    default:
      return (
        <>
          <mesh position={[0, 1.25, 0]}><dodecahedronGeometry args={[0.62, 0]} />{primary}</mesh>
          <mesh position={[0, 1.92, 0]}><icosahedronGeometry args={[0.42, 1]} />{accent}</mesh>
          {[-0.27, 0.27].map((x) => (
            <mesh key={x} position={[x, 0.48, 0]}>
              <cylinderGeometry args={[0.14, 0.18, 0.75, 6]} />{primary}
            </mesh>
          ))}
        </>
      );
  }
}

function isPlayerOrPartyNear(points: readonly MobVector3[], maxDistance: number): boolean {
  const player = useGameStore.getState().playerPosition;
  const partyMemberIds = usePartyStore.getState().members.map((member) => member.id);
  const maxDistanceSquared = maxDistance * maxDistance;

  return points.some((point) => {
    const dx = player[0] - point[0];
    const dy = player[1] - point[1];
    const dz = player[2] - point[2];
    return dx * dx + dy * dy + dz * dz <= maxDistanceSquared
      || partyMemberIds.some((id) => partyRuntimeDistanceSquared(id, point) <= maxDistanceSquared);
  });
}

function stepToward(
  current: MutableMobVector3,
  target: MobVector3,
  maximumStep: number,
): number {
  const dx = target[0] - current[0];
  const dy = target[1] - current[1];
  const dz = target[2] - current[2];
  const distance = Math.hypot(dx, dy, dz);
  if (distance <= 0.001) return 0;
  const step = Math.min(Math.max(0, maximumStep), distance);
  current[0] += dx / distance * step;
  current[1] += dy / distance * step;
  current[2] += dz / distance * step;
  return distance;
}

/**
 * Activates content when the player or an autonomous party member approaches.
 * The stores are read
 * imperatively and React state changes only when crossing the distance boundary,
 * so the player's 10 Hz HUD snapshot does not re-render the population tree.
 */
function usePopulationProximity(points: readonly MobVector3[]): boolean {
  const initial = isPlayerOrPartyNear(points, POPULATION_ACTIVATION_DISTANCE);
  const nearbyRef = useRef(initial);
  const [nearby, setNearby] = useState(initial);
  const elapsedRef = useRef(0);

  useFrame((_, delta) => {
    elapsedRef.current += delta;
    if (elapsedRef.current < PROXIMITY_CHECK_INTERVAL) return;
    elapsedRef.current = 0;

    // Small hysteresis prevents mount/unmount churn at the boundary.
    const distance = POPULATION_ACTIVATION_DISTANCE + (nearbyRef.current ? 18 : 0);
    const next = isPlayerOrPartyNear(points, distance);
    if (next === nearbyRef.current) return;
    nearbyRef.current = next;
    setNearby(next);
  });

  return nearby;
}

function ProxyMob({
  definition,
  instanceId,
  active,
  moving,
  showLabel,
  useOriginalModels,
}: ProxyMobProps) {
  const mob = useCombatStore((state) => state.mobs[instanceId]);
  const selected = useCombatStore((state) => state.selectedTargetId === instanceId);
  const showAggroRanges = useCombatStore((state) => state.settings.showMobAggroRanges);
  const gameMode = useGameStore((state) => state.gameMode);
  const characterLevel = useCombatStore((state) => state.level);
  const manastormLevel = useManastormStore((state) => state.level);
  const manastormPartySize = useManastormStore((state) => state.partySize);
  const manastormModeId = useManastormStore((state) => state.modeId);
  const manastormMapRewardMultiplier = useManastormStore(
    (state) => state.currentEncounter?.mapRewardMultiplier ?? 1,
  );
  const boss = definition.kind === "boss";
  const manastormModifiers = gameMode === "manastorm"
    ? manastormRuntimeScaling(manastormLevel, manastormPartySize)
    : null;
  const { primaryColor, accentColor, scale, archetype } = definition.visual;
  const model = useOriginalModels ? definition.visual.model : undefined;
  const proceduralModelAnimation = model?.animationMode === "procedural";
  const labelHeight = model?.labelHeight ?? (boss ? 3.25 : 2.65) * scale;
  const markerRadius = model?.markerRadius ?? 0.9 * scale;
  const dead = mob?.dead ?? false;
  const lootable = Boolean(dead && mob?.hasLoot && !mob.looted);
  const attackRevision = mob?.attackRevision ?? 0;
  const attackAnimation = mob?.attackAnimation ?? "attack";
  const woundRevision = mob?.woundRevision ?? 0;
  const statusNow = Date.now();
  const movementLocked = mob?.statuses.some((status) => status.endsAt > statusNow
    && MOVEMENT_LOCKING_MOB_STATUSES.has(status.kind)) ?? false;
  const feared = mob?.statuses.some((status) => status.endsAt > statusNow
    && status.kind === "fear") ?? false;
  const animationState = resolveMobAnimationState({
    dead,
    engaged: Boolean(mob?.engaged),
    combatPhase: mob?.combatPhase ?? "idle",
    moving,
    feared,
    movementLocked,
  });
  const forceFullFidelity = boss || selected || dead || Boolean(mob?.engaged);

  useEffect(() => {
    const baseHealthMultiplier = definition.combat?.healthMultiplier ?? 1;
    const baseDamageMultiplier = definition.combat?.damageMultiplier ?? 1;
    useCombatStore.getState().registerMob(instanceId, {
      name: definition.name,
      serverEntry: definition.combat?.serverEntry,
      boss,
      level: gameMode === "manastorm"
        ? definition.combat?.level
        : scaledDungeonEnemyLevel(characterLevel, boss),
      hasLoot: definition.hasLoot ?? (boss && bossHasLoot(definition.id)),
      lootSourceId: boss ? definition.id : undefined,
      healthMultiplier: baseHealthMultiplier * (manastormModifiers?.enemyStatMultiplier ?? 1),
      damageMultiplier: baseDamageMultiplier * (manastormModifiers?.enemyStatMultiplier ?? 1),
      bonusLootChance: manastormModifiers?.bonusLootChance,
      moveSpeed: definition.combat?.moveSpeed,
      aggroRange: definition.combat?.aggroRange,
      leashRange: definition.combat?.leashRange,
      attacks: definition.combat?.attacks,
      xpReward: gameMode === "manastorm"
        ? manastormExperienceReward(
          boss ? 110 + characterLevel * 35 : 28 + characterLevel * 7,
          manastormLevel,
          manastormPartySize,
          manastormMapRewardMultiplier,
          undefined,
          manastormModeId,
        )
        : undefined,
    });
  }, [
    boss,
    characterLevel,
    definition.hasLoot,
    definition.id,
    definition.name,
    definition.combat,
    instanceId,
    gameMode,
    manastormLevel,
    manastormMapRewardMultiplier,
    manastormModeId,
    manastormPartySize,
    manastormModifiers?.bonusLootChance,
    manastormModifiers?.enemyStatMultiplier,
  ]);
  if (mob?.despawned) return null;
  const proxyBody = (
    <group scale={scale}>
      <ProxyBody archetype={archetype} primaryColor={primaryColor} accentColor={accentColor} />
    </group>
  );

  return (
    <group
      onPointerDown={(event) => {
        if (event.button !== PRIMARY_MOUSE_BUTTON) return;
        event.stopPropagation();
        if (dead) {
          useCombatStore.getState().lootMob(instanceId);
          return;
        }
        usePartyStore.getState().clearSelection();
        useCombatStore.getState().selectMob(instanceId);
      }}
    >
      <AnimatedMobBody
        active={active}
        archetype={archetype}
        animationState={animationState}
        attackRevision={attackRevision}
        woundRevision={woundRevision}
        instanceId={instanceId}
        motionWeight={model ? (proceduralModelAnimation ? 1 : 0.18) : 1}
        proceduralCombatFallback={!model || proceduralModelAnimation}
        forceFullFidelity={forceFullFidelity}
      >
        {model ? (
          <CreatureModelErrorBoundary fallback={proxyBody} resetKey={model.url}>
            <Suspense fallback={proxyBody}>
              <OriginalCreatureModel
                model={model}
                active={active}
                proceduralAnimation={proceduralModelAnimation}
                animationState={animationState}
                attackRevision={attackRevision}
                attackAnimation={attackAnimation}
                woundRevision={woundRevision}
                forceFullFidelity={forceFullFidelity}
              />
            </Suspense>
          </CreatureModelErrorBoundary>
        ) : (
          proxyBody
        )}
      </AnimatedMobBody>

      {showAggroRanges && gameMode === "dungeon" && mob && !dead && !mob.engaged ? (
        <mesh
          name={`mob-aggro-range-${instanceId}`}
          position={[0, 0.045, 0]}
          rotation={[Math.PI / 2, 0, 0]}
          renderOrder={2}
        >
          <torusGeometry args={[mob.aggroRange, boss ? 0.075 : 0.05, 4, 56]} />
          <meshBasicMaterial
            color={boss ? "#e59a55" : "#c96b55"}
            transparent
            opacity={boss ? 0.62 : 0.5}
            depthTest={false}
            depthWrite={false}
          />
        </mesh>
      ) : null}

      {boss && !dead ? (
        <>
          <mesh position={[0, 0.08, 0]} rotation={[Math.PI / 2, 0, 0]}>
            <torusGeometry args={[markerRadius, Math.min(0.12, markerRadius * 0.08), 6, 24]} />
            <meshStandardMaterial color={accentColor} emissive={accentColor} emissiveIntensity={1.3} />
          </mesh>
          <mesh position={[0, Math.max(2.43 * scale, labelHeight - 0.65), 0]}>
            <coneGeometry args={[0.48, 0.55, 5]} />
            <meshStandardMaterial color={accentColor} emissive={accentColor} emissiveIntensity={0.35} />
          </mesh>
        </>
      ) : null}

      {selected && !dead ? (
        <mesh position={[0, 0.095, 0]} rotation={[Math.PI / 2, 0, 0]}>
          <torusGeometry args={[markerRadius * 1.12, Math.min(0.09, markerRadius * 0.07), 6, 28]} />
          <meshBasicMaterial color="#f7df84" transparent opacity={0.95} depthWrite={false} />
        </mesh>
      ) : null}

      <MobHealthPlate instanceId={instanceId} height={labelHeight - (showLabel ? 0.38 : 0)} boss={boss} />

      {showLabel && !dead ? (
        <Html
          center
          position={[0, labelHeight, 0]}
          distanceFactor={13}
          style={boss ? bossLabelStyle : mobLabelStyle}
        >
          <div>
            <div>{definition.name}</div>
            {boss ? <div style={{ fontSize: "0.78em", fontWeight: 600, opacity: 0.85 }}>{definition.title}</div> : null}
          </div>
        </Html>
      ) : null}
      {lootable ? (
        <Html
          center
          position={[0, Math.max(2.2 * scale, labelHeight - 0.35), 0]}
          distanceFactor={12}
          style={lootLabelStyle}
          zIndexRange={[80, 0]}
        >
          <div>Loot {definition.name} · {mob?.pendingLoot.length ?? 0} item{mob?.pendingLoot.length === 1 ? "" : "s"}</div>
        </Html>
      ) : null}
    </group>
  );
}

function MobHealthPlate({ instanceId, height, boss }: {
  readonly instanceId: string;
  readonly height: number;
  readonly boss: boolean;
}) {
  const plateRef = useRef<Group>(null);
  const castFillRef = useRef<Mesh>(null);
  const mob = useCombatStore((state) => state.mobs[instanceId]);
  const selected = useCombatStore((state) => state.selectedTargetId === instanceId);
  const enabled = useCombatStore((state) => state.settings.showMobHealthBars);
  useFrame(({ camera }) => {
    if (plateRef.current) plateRef.current.lookAt(camera.position);
    if (castFillRef.current && mob?.activeCast) {
      const duration = Math.max(1, mob.activeCast.completesAt - mob.activeCast.startedAt);
      const now = Date.now();
      const progress = Math.max(0.001, Math.min(1, (now - mob.activeCast.startedAt) / duration));
      castFillRef.current.scale.x = progress;
      castFillRef.current.position.x = -width * (1 - progress) / 2;
    }
  });
  if (!enabled || !mob || mob.dead) return null;
  const width = boss ? 2.25 : 1.5;
  const healthPercent = Math.max(0, Math.min(1, mob.health / mob.maxHealth));
  const fillWidth = width * healthPercent;
  return (
    <group ref={plateRef} position={[0, height, 0]} renderOrder={7}>
      <mesh position={[0, 0, 0]}>
        <planeGeometry args={[width + 0.12, boss ? 0.24 : 0.18]} />
        <meshBasicMaterial color={selected ? "#e8cf7d" : "#10100d"} depthTest depthWrite={false} />
      </mesh>
      <mesh position={[-(width - fillWidth) / 2, 0, 0.008]}>
        <planeGeometry args={[Math.max(0.001, fillWidth), boss ? 0.14 : 0.1]} />
        <meshBasicMaterial color={boss ? "#b54c37" : "#8f322d"} depthTest depthWrite={false} />
      </mesh>
      {mob.activeCast ? (
        <>
          <mesh position={[0, boss ? -0.22 : -0.18, 0.004]}>
            <planeGeometry args={[width + 0.08, boss ? 0.13 : 0.11]} />
            <meshBasicMaterial color="#0b1014" depthTest depthWrite={false} />
          </mesh>
          <mesh ref={castFillRef} position={[-width / 2, boss ? -0.22 : -0.18, 0.012]}>
            <planeGeometry args={[width, boss ? 0.085 : 0.07]} />
            <meshBasicMaterial
              color={mob.activeCast.interruptible ? "#e0ae42" : "#777b82"}
              depthTest
              depthWrite={false}
            />
          </mesh>
          <Html
            center
            position={[0, boss ? -0.44 : -0.37, 0.014]}
            distanceFactor={18}
            style={{ ...mobLabelStyle, color: mob.activeCast.interruptible ? "#ffe6a2" : "#c7cbd0", fontSize: "9px" }}
            zIndexRange={[72, 0]}
          >
            <div>{mob.activeCast.name}</div>
          </Html>
        </>
      ) : null}
      <Html
        center
        position={[0, mob.activeCast ? (boss ? -0.6 : -0.52) : boss ? -0.3 : -0.24, 0.012]}
        distanceFactor={18}
        style={{ pointerEvents: "none" }}
        zIndexRange={[70, 0]}
      >
        <TimedEffectStrip
          targetId={instanceId}
          kinds={["dot"]}
          className="timed-effect-strip--world"
        />
        <AuraStrip targetId={instanceId} className="aura-strip--world" />
      </Html>
    </group>
  );
}

interface RoamingPackProps {
  readonly definition: MobPackDefinition;
  readonly entities: PopulationDefinitionMap;
  readonly active: boolean;
  readonly showLabels: boolean;
  readonly useOriginalModels: boolean;
  readonly allowedRuntimeIds: ReadonlySet<string> | null;
}

function RoamingPack({
  definition,
  entities,
  active,
  showLabels,
  useOriginalModels,
  allowedRuntimeIds,
}: RoamingPackProps) {
  const path = useMemo(() => createLoopedPath(definition.waypoints), [definition.waypoints]);
  const distanceRef = useRef(definition.startDistance ?? 0);
  const { scratchPositions, scratchForwards, chasePositions } = useMemo(() => {
    const positions: MutableMobVector3[] = definition.members.map(() => [0, 0, 0]);
    const forwards: MutableMobVector3[] = definition.members.map(() => [0, 0, 1]);

    samplePackFormation(
      path,
      definition.startDistance ?? 0,
      definition.members,
      positions,
      forwards,
    );

    return {
      scratchPositions: positions,
      scratchForwards: forwards,
      chasePositions: positions.map((position) => [...position] as MutableMobVector3),
    };
  }, [definition.members, definition.startDistance, path]);
  // Use the pack's sampled positions as wake-up targets. A long route should
  // not keep actors mounted merely because the player is near another endpoint.
  const nearby = usePopulationProximity(scratchPositions);
  const memberRefs = useRef<Array<Group | null>>([]);
  const chasingRefs = useRef<boolean[]>(definition.members.map(() => false));

  useEffect(() => {
    const cleanups = definition.members.map((member, index) => registerMobRuntime(
      `${definition.id}:${member.id}`,
      scratchPositions[index],
    ));
    return () => cleanups.forEach((cleanup) => cleanup());
  }, [definition.id, definition.members, scratchPositions]);

  useFrame((_, delta) => {
    if (!nearby) return;

    const combat = useCombatStore.getState();
    const playerPosition = useGameStore.getState().playerPosition;
    const now = Date.now();
    const anyEngaged = definition.members.some((member) => combat.mobs[`${definition.id}:${member.id}`]?.engaged);
    if (active && !anyEngaged) {
      let movementMultiplier = 1;
      for (const member of definition.members) {
        const statuses = combat.mobs[`${definition.id}:${member.id}`]?.statuses ?? [];
        if (statuses.some((status) => status.endsAt > now && (
          MOVEMENT_LOCKING_MOB_STATUSES.has(status.kind) || status.kind === "fear"
        ))) {
          movementMultiplier = 0;
          break;
        }
        const slow = statuses.find((status) => status.endsAt > now && status.kind === "slow");
        if (slow) movementMultiplier = Math.min(movementMultiplier, 1 - (slow.magnitude ?? 0.5));
      }
      distanceRef.current = (distanceRef.current + Math.min(delta, 0.05) * definition.speed * movementMultiplier)
        % path.totalLength;
    }

    samplePackFormation(
      path,
      distanceRef.current,
      definition.members,
      scratchPositions,
      scratchForwards,
    );

    for (let index = 0; index < definition.members.length; index += 1) {
      const patrolPosition = scratchPositions[index];
      const forward = scratchForwards[index];
      const member = definition.members[index];
      const instanceId = `${definition.id}:${member.id}`;
      const mob = combat.mobs[instanceId];
      const chasePosition = chasePositions[index];
      const engaged = Boolean(mob?.engaged && !mob.dead);
      const targetPosition = mobTargetPosition(mob?.targetActorId, playerPosition);
      if (engaged && !chasingRefs.current[index]) {
        const object = memberRefs.current[index];
        const start = object?.position;
        chasePosition[0] = start?.x ?? patrolPosition[0];
        chasePosition[1] = start?.y ?? patrolPosition[1];
        chasePosition[2] = start?.z ?? patrolPosition[2];
      }
      chasingRefs.current[index] = engaged;

      let position = patrolPosition;
      if (mob?.dead) {
        // Keep the corpse at the final combat position while its one-shot
        // death clip reaches and holds the terminal frame.
        position = chasePosition;
      } else if (engaged) {
        const statuses = mob!.statuses;
        const movementLocked = statuses.some((status) => status.endsAt > now
          && MOVEMENT_LOCKING_MOB_STATUSES.has(status.kind));
        const feared = statuses.some((status) => status.endsAt > now && status.kind === "fear");
        const slow = statuses.find((status) => status.endsAt > now && status.kind === "slow")?.magnitude ?? 0;
        const maximumStep = Math.min(delta, 0.05)
          * mob!.moveSpeed
          * Math.max(0.1, 1 - slow);
        if (!movementLocked && feared) {
          stepFearedMobAway(
            chasePosition,
            targetPosition,
            mob!.homePosition ?? patrolPosition,
            maximumStep * 1.1,
            mob!.leashRange,
            chasePosition,
          );
        } else if (!movementLocked && mob!.combatPhase === "chasing") {
          stepToward(
            chasePosition,
            targetPosition,
            maximumStep,
          );
        }
        position = chasePosition;
        const facingSign = feared && !movementLocked ? -1 : 1;
        forward[0] = (targetPosition[0] - position[0]) * facingSign;
        forward[2] = (targetPosition[2] - position[2]) * facingSign;
      } else if (mob?.combatPhase === "returning") {
        stepToward(chasePosition, patrolPosition, Math.min(delta, 0.05) * mob.moveSpeed * 1.25);
        position = chasePosition;
      } else {
        chasePosition[0] = patrolPosition[0];
        chasePosition[1] = patrolPosition[1];
        chasePosition[2] = patrolPosition[2];
      }
      updateMobRuntimePosition(
        instanceId,
        position[0],
        position[1],
        position[2],
      );
      const object = memberRefs.current[index];
      if (!object) continue;
      object.position.set(position[0], position[1], position[2]);
      object.rotation.y = Math.atan2(forward[0], forward[2]);
    }
  });

  if (!nearby) return null;

  return (
    <group name={`mob-pack-${definition.id}`}>
      {definition.members.map((member, index) => {
        if (allowedRuntimeIds && !allowedRuntimeIds.has(`${definition.id}:${member.id}`)) return null;
        const entity = entities[member.entityId];
        if (!entity) return null;

        return (
          <group
            key={member.id}
            ref={(object) => {
              memberRefs.current[index] = object;
            }}
          >
            <ProxyMob
              definition={entity}
              instanceId={`${definition.id}:${member.id}`}
              active={active}
              moving
              showLabel={showLabels && entity.kind === "boss"}
              useOriginalModels={useOriginalModels}
            />
          </group>
        );
      })}
    </group>
  );
}

interface StaticPopulationProps {
  readonly spawn: StaticMobSpawnDefinition;
  readonly entities: PopulationDefinitionMap;
  readonly active: boolean;
  readonly showLabels: boolean;
  readonly useOriginalModels: boolean;
}

function StaticPopulationMember({ spawn, entities, active, showLabels, useOriginalModels }: StaticPopulationProps) {
  const entity = entities[spawn.entityId];
  const groupRef = useRef<Group>(null);
  const positionRef = useRef<MutableMobVector3>([...spawn.position]);
  useEffect(() => registerMobRuntime(spawn.id, spawn.position), [spawn.id, spawn.position]);
  useFrame((_, delta) => {
    const mob = useCombatStore.getState().mobs[spawn.id];
    const position = positionRef.current;
    const player = useGameStore.getState().playerPosition;
    const targetPosition = mobTargetPosition(mob?.targetActorId, player);
    const now = Date.now();
    let feared = false;
    let movementLocked = false;
    if (mob?.dead) {
      // Preserve the final combat position for the held death pose.
    } else if (mob?.engaged) {
      movementLocked = mob.statuses.some((status) => status.endsAt > now
        && MOVEMENT_LOCKING_MOB_STATUSES.has(status.kind));
      feared = mob.statuses.some((status) => status.endsAt > now && status.kind === "fear");
      const slow = mob.statuses.find((status) => status.endsAt > now && status.kind === "slow")?.magnitude ?? 0;
      const maximumStep = Math.min(delta, 0.05) * mob.moveSpeed * Math.max(0.1, 1 - slow);
      if (!movementLocked && feared) {
        stepFearedMobAway(
          position,
          targetPosition,
          mob.homePosition ?? spawn.position,
          maximumStep * 1.1,
          mob.leashRange,
          position,
        );
      } else if (!movementLocked && mob.combatPhase === "chasing") {
        stepToward(position, targetPosition, maximumStep);
      }
    } else if (mob?.combatPhase === "returning") {
      stepToward(position, spawn.position, Math.min(delta, 0.05) * mob.moveSpeed * 1.25);
    } else {
      position[0] = spawn.position[0];
      position[1] = spawn.position[1];
      position[2] = spawn.position[2];
    }
    updateMobRuntimePosition(spawn.id, position[0], position[1], position[2]);
    if (groupRef.current) {
      groupRef.current.position.set(position[0], position[1], position[2]);
      if (mob?.engaged) {
        const facingSign = feared && !movementLocked ? -1 : 1;
        groupRef.current.rotation.y = Math.atan2(
          (targetPosition[0] - position[0]) * facingSign,
          (targetPosition[2] - position[2]) * facingSign,
        );
      }
    }
  });
  if (!entity) return null;

  return (
    <group ref={groupRef} name={`mob-spawn-${spawn.id}`} position={spawn.position} rotation={[0, spawn.yaw ?? 0, 0]}>
      <ProxyMob
        definition={entity}
        instanceId={spawn.id}
        active={active}
        moving={(() => {
          const phase = useCombatStore.getState().mobs[spawn.id]?.combatPhase;
          return phase === "chasing" || phase === "returning";
        })()}
        showLabel={showLabels && entity.kind === "boss"}
        useOriginalModels={useOriginalModels}
      />
    </group>
  );
}

function StaticPopulationCluster({
  id,
  points,
  spawns,
  entities,
  active,
  showLabels,
  useOriginalModels,
}: {
  readonly id: string;
  readonly points: readonly MobVector3[];
  readonly spawns: readonly StaticMobSpawnDefinition[];
  readonly entities: PopulationDefinitionMap;
  readonly active: boolean;
  readonly showLabels: boolean;
  readonly useOriginalModels: boolean;
}) {
  const nearby = usePopulationProximity(points);
  if (!nearby) return null;
  return (
    <group name={`static-mob-cluster-${id}`}>
      {spawns.map((spawn) => (
        <StaticPopulationMember
          key={spawn.id}
          spawn={spawn}
          entities={entities}
          active={active}
          showLabels={showLabels}
          useOriginalModels={useOriginalModels}
        />
      ))}
    </group>
  );
}

export interface MobPopulationProps {
  readonly entities: PopulationDefinitionMap;
  readonly roamingPacks: readonly MobPackDefinition[];
  readonly staticSpawns: readonly StaticMobSpawnDefinition[];
  readonly active?: boolean;
  readonly showLabels?: boolean;
  readonly useOriginalModels?: boolean;
  /** When supplied, only these runtime spawn ids are presented and registered. */
  readonly allowedRuntimeIds?: readonly string[];
}

/**
 * Standalone visualization runtime. It owns no gameplay state and performs all
 * high-frequency motion by mutating Three object refs inside useFrame.
 */
export function MobPopulation({
  entities,
  roamingPacks,
  staticSpawns,
  active = true,
  showLabels = true,
  useOriginalModels = true,
  allowedRuntimeIds,
}: MobPopulationProps) {
  const allowedSet = useMemo(
    () => allowedRuntimeIds ? new Set(allowedRuntimeIds) : null,
    [allowedRuntimeIds],
  );
  const staticClusters = useMemo(() => clusterStaticMobSpawns(
    allowedSet ? staticSpawns.filter((spawn) => allowedSet.has(spawn.id)) : staticSpawns,
  ), [allowedSet, staticSpawns]);
  return (
    <group name="mob-population">
      {roamingPacks.map((pack) => (
        <RoamingPack
          key={pack.id}
          definition={pack}
          entities={entities}
          active={active}
          showLabels={showLabels}
          useOriginalModels={useOriginalModels}
          allowedRuntimeIds={allowedSet}
        />
      ))}
      {staticClusters.map((cluster) => (
        <StaticPopulationCluster
          key={cluster.id}
          id={cluster.id}
          points={cluster.points}
          spawns={cluster.spawns}
          entities={entities}
          active={active}
          showLabels={showLabels}
          useOriginalModels={useOriginalModels}
        />
      ))}
    </group>
  );
}
