> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spurdoverse.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Building an ESP

> Health elements in the ESP builder, boxes on the overlay and a menu toggle, step by step.

We'll build an enemy ESP in five small steps: health text and a health bar in the ESP builder,
then boxes and head dots drawn on the overlay, colored by visibility and controlled from a menu.

<Note>
  Start from a project made with `spd new` (see [Getting started](/)) and keep `spd watch`
  running. The script reloads on every save, so each step shows up in game right away.
</Note>

***

## Step 1: Health text

[esp](/api/esp) elements live in the user's ESP builder. The user places and toggles them;
your code only supplies the value. Replace `src/index.ts` with:

```ts theme={null}
import esp from "@native/esp";

const enemies = esp.class("enemy");

enemies.text({
    name: "health",
    area: "top",
    preview: "100 hp",
    evaluate: (pawn) => (pawn.m_iHealth > 0 ? `${pawn.m_iHealth} hp` : null),
});
```

`evaluate` runs every tick for every enemy pawn. Returning `null` hides the element on that pawn.
Open the ESP builder: "health" is there, showing the `preview` text.

***

## Step 2: Health bar

A bar takes a fraction, or an object when you want to set the colors. Add:

```ts theme={null}
enemies.bar({
    name: "health-bar",
    label: "Health bar",
    area: "left",
    preview: 1,
    evaluate: (pawn) => {
        const hp = pawn.m_iHealth;
        if (hp <= 0) return null;
        return { value: hp, max: 100, color: "#ff4040", colorAlt: "#40ff40", colorMode: "value" };
    },
});
```

`colorMode: "value"` blends from `color` when empty to `colorAlt` when full. Keys you leave out
keep whatever the user set in the builder.

<Tip>
  When several elements need the same data, compute it once per pawn with `context`:
  `esp.class("enemy", { context: (pawn) => ({ hp: pawn.m_iHealth }) })`, then read
  `ctx.hp` in each `evaluate(pawn, ctx)`.
</Tip>

***

## Step 3: Boxes on the overlay

Builder elements are laid out for you. For free-form drawing, use [render](/api/render).
Reading entities goes in `tick`, drawing in `render`: draw calls throw anywhere else.

Add the imports at the top and the listeners at the bottom:

```ts theme={null}
import render from "@native/render";
import entities from "@native/entities";

let targets: IPlayerPawn[] = [];

on("tick", () => {
    targets = [];
    const me = entities.getLocalPlayer();
    if (!me) return;

    for (const pawn of entities.getPlayers({ skipLocal: true })) {
        if (pawn.m_iHealth > 0 && pawn.isEnemy(me)) targets.push(pawn);
    }
});

on("render", () => {
    for (const pawn of targets) {
        const box = render.boundsOf(pawn);
        if (box) render.rect(box.min, box.max, "#ff4040", { thickness: 1.5 });
    }
});
```

[`render.boundsOf`](/api/render#boundsof) returns the pawn's box on screen, or `null` when part
of it is behind the camera.

***

## Step 4: Head dot and visibility

Store whether each enemy is visible, and pick the color from it. Replace both listeners:

```ts theme={null}
let targets: { pawn: IPlayerPawn; visible: boolean }[] = []; // [!code ++]

on("tick", () => {
    targets = [];
    const me = entities.getLocalPlayer();
    if (!me) return;

    for (const pawn of entities.getPlayers({ skipLocal: true })) {
        if (pawn.m_iHealth <= 0 || !pawn.isEnemy(me)) continue; // [!code ++]
        targets.push({ pawn, visible: me.isVisible(pawn) === true }); // [!code ++]
    }
});

on("render", () => {
    for (const { pawn, visible } of targets) { // [!code ++]
        const box = render.boundsOf(pawn);
        if (!box) continue; // [!code ++]

        const color = visible ? "#40ff40" : "#ff4040"; // [!code ++]
        render.rect(box.min, box.max, color, { thickness: 1.5 }); // [!code ++]

        const eye = pawn.getEyePosition(); // [!code ++]
        const head = eye && render.worldToScreen(eye); // [!code ++]
        if (head) render.circleFilled(head, 3, color); // [!code ++]
    }
});
```

`isVisible` returns `null` without a map, hence the `=== true`. [`render.worldToScreen`](/api/render#worldtoscreen) returns `null`
for points behind the camera.

***

## Step 5: Menu toggle

Give the script a page in the menu with a switch and two colors. Values persist in the user's
configs. Declare controls once, at the top level:

```ts theme={null}
import ui from "@native/ui";

const settings = ui.page("ESP", { icon: "eye" }).group("Boxes");
const boxes = settings.switch("Enabled", { default: true });
const visibleColor = settings.color("Visible", { default: "#40ff40" });
const hiddenColor = settings.color("Hidden", { default: "#ff4040" });

on("keydown", (e) => {
    if (e.code === "F6" && !e.repeat) boxes.value = !boxes.value;
});
```

Then read them in the listeners:

```ts theme={null}
// tick
if (!me || !boxes.value) return; // [!code ++]

// render
const color = (visible ? visibleColor.value : hiddenColor.value) ?? "#ffffff"; // [!code ++]
```

Pressing F6 flips the switch in the menu too.

***

## Full script

<Accordion title="src/index.ts">
  ```ts theme={null}
  import esp from "@native/esp";
  import ui from "@native/ui";
  import render from "@native/render";
  import entities from "@native/entities";

  const settings = ui.page("ESP", { icon: "eye" }).group("Boxes");
  const boxes = settings.switch("Enabled", { default: true });
  const visibleColor = settings.color("Visible", { default: "#40ff40" });
  const hiddenColor = settings.color("Hidden", { default: "#ff4040" });

  const enemies = esp.class("enemy");

  enemies.text({
      name: "health",
      area: "top",
      preview: "100 hp",
      evaluate: (pawn) => (pawn.m_iHealth > 0 ? `${pawn.m_iHealth} hp` : null),
  });

  enemies.bar({
      name: "health-bar",
      label: "Health bar",
      area: "left",
      preview: 1,
      evaluate: (pawn) => {
          const hp = pawn.m_iHealth;
          if (hp <= 0) return null;
          return { value: hp, max: 100, color: "#ff4040", colorAlt: "#40ff40", colorMode: "value" };
      },
  });

  let targets: { pawn: IPlayerPawn; visible: boolean }[] = [];

  on("tick", () => {
      targets = [];
      const me = entities.getLocalPlayer();
      if (!me || !boxes.value) return;

      for (const pawn of entities.getPlayers({ skipLocal: true })) {
          if (pawn.m_iHealth <= 0 || !pawn.isEnemy(me)) continue;
          targets.push({ pawn, visible: me.isVisible(pawn) === true });
      }
  });

  on("render", () => {
      for (const { pawn, visible } of targets) {
          const box = render.boundsOf(pawn);
          if (!box) continue;

          const color = (visible ? visibleColor.value : hiddenColor.value) ?? "#ffffff";
          render.rect(box.min, box.max, color, { thickness: 1.5 });

          const eye = pawn.getEyePosition();
          const head = eye && render.worldToScreen(eye);
          if (head) render.circleFilled(head, 3, color);
      }
  });

  on("keydown", (e) => {
      if (e.code === "F6" && !e.repeat) boxes.value = !boxes.value;
  });
  ```
</Accordion>

***

## Next steps

<CardGroup cols={2}>
  <Card title="esp" icon="eye" href="/api/esp">
    Icons, style keys, `context`, element handles.
  </Card>

  <Card title="render" icon="pen" href="/api/render">
    Every draw call, text, textures.
  </Card>

  <Card title="ui" icon="sliders" href="/api/ui">
    Sliders, combos, hotkeys, reactive labels.
  </Card>

  <Card title="entities" icon="users" href="/api/entities">
    Bones, hitboxes, schema fields.
  </Card>
</CardGroup>
