# Desk Pals pack format (version 1)

A pet is a **folder** with a `pal.json` and some PNG frames. Drop the folder into
`~/.local/share/deskpals/packs/` (or the extension's own `packs/`), and it shows up
in the Desk Pals settings with its own "how many" counter. Folders starting with `_`
or `.` are ignored (use `_src/` for your art scripts).

Check a pack before sharing it:

```bash
gjs -m tools/validate-pack.js path/to/your-pack
```

The validator lists every problem at once. The same checks run when the extension loads
packs; a bad pack is skipped (and listed in the settings window), never fatal.

## pal.json

```json
{
  "format": 1,
  "id": "sheep",
  "name": "Sheep",
  "author": "You",
  "license": "CC0-1.0",
  "description": "One line shown in settings.",
  "sprite": {"width": 72, "height": 60, "scale": 1.5, "feetPad": 2, "facing": "right"},
  "speed": 55,
  "abilities": {"jump": true, "maxUp": 380, "maxReach": 280},
  "animations": {
    "walk":  {"frames": ["walk0.png", "walk1.png"], "fps": 7},
    "graze": {"frames": ["graze0.png", "graze1.png"], "fps": 2}
  },
  "states": {
    "walk":  {"animation": "walk", "move": true, "duration": [4, 12], "next": {"graze": 5, "walk": 3}},
    "graze": {"animation": "graze", "duration": [2.5, 6], "next": {"walk": 1}}
  },
  "bubbles": {"lines": ["Baa."], "interval": [30, 90], "duration": [4, 6]}
}
```

| Field | Meaning |
|---|---|
| `format` | Always `1`. |
| `id` | Lowercase letters, digits, dashes (max 32). Must be unique; a pack in your user folder replaces a built-in one with the same id. |
| `name`, `author`, `license`, `description` | Shown in settings. `name` is required. |
| `sprite.width/height` | Pixel size of **every** frame (8-512). All frames must be exactly this size. |
| `sprite.scale` | Default display scale (0.25-8). Each pet's own size setting multiplies this. Pick a scale so pets of different packs come out a similar height (about 55-65 px). |
| `sprite.feetPad` | Empty pixel rows under the feet in your PNGs, so the feet land on the ground. |
| `sprite.facing` | Which way the art faces: `right` (default) or `left`. The engine mirrors it when the pet turns. |
| `speed` | Walking speed in px/second (before the user's Speed % setting). |
| `abilities` | `jump` (default true), `maxUp` (px it can jump up, default 380), `maxReach` (px it can jump sideways, default 280). |

### animations

A named list of frames played in a loop at `fps` (0.5-60). Frames are plain `.png` file names in
the pack folder (no sub-folders). Max 64 frames in total, 2 MB per file.

Three animation names are used by the engine itself; all are optional and fall back to the
first `move` state's animation:

- `air` - while jumping or falling
- `squash` - the moment of landing (the engine also squashes the sprite)
- `held` - while the user is holding the pet with the mouse

### states

What the pet does when it is standing on something. After a state's `duration` (min, max seconds)
runs out, the next state is picked at random using `next` (state name -> weight; omit it to pick
any state by its `weight`, default 1).

- `animation` - one of your animations.
- `move: true` - the pet walks while in this state. At least one state must move.
- State names are lowercase; `air`, `squash` and `held` are reserved.

### bubbles (optional): what the pet says

Thought bubbles pop up now and then (every `interval` seconds, min/max, scaled by the user's
Talkativeness setting) and when something happens. They stay for `duration` seconds.

```json
"bubbles": {
  "interval": [25, 70],
  "duration": [4, 6],
  "lines": ["Baa?", "I forgot what I was doing."],
  "topics": {
    "app":    ["{app}? Can I eat {app}?"],
    "pickup": ["Put me down!"],
    "pet":    ["Is {pet} a sheep?"]
  }
}
```

`lines` are plain random remarks (same as the `random` topic). `topics` are remarks about a
specific thing, picked when that thing is true or happens. A line is only used when every
`{placeholder}` in it has a value at that moment, so `{app}` lines wait for a focused window.
Each line is up to 100 characters; each topic has 1-100 lines.

| Topic | When | Placeholders you can use |
|---|---|---|
| `random` | any time | any of the ambient ones below |
| `user` | any time | `{user}` (the logged-in user's first name) |
| `time`, `night` | any time / late at night | `{time}` `{hour}` |
| `font`, `theme`, `screen` | any time | `{font}` `{theme}` (dark/light) `{res}` `{monitors}` |
| `app`, `title` | the focused application / its window title | `{app}` `{title}` |
| `windows` | two or more windows open | `{n}` |
| `fullscreen` | a fullscreen window has focus | `{app}` |
| `window-opened`, `window-closed` | a window appears / goes away | `{app}` |
| `pet` | any time, about another pet on screen | `{pet}` (its name) |
| `pet:<id>` | the same, but only about the pack with that id, e.g. `pet:cat` | `{pet}` |
| `joined`, `left` | a pet is added / removed (`joined:<id>` / `left:<id>` for one specific pack) | `{pet}` |
| `speed-up`, `speed-down` | the user changes the speed setting | |
| `bigger`, `smaller` | this pet is resized | |
| `mouse-fast`, `mouse-idle`, `mouse-click` | wild mouse swipe / no mouse for 2 minutes / click flurry | |
| `mouse-near` | the cursor comes close to this pet | |
| `pickup`, `drop` | the user picks this pet up / lets go | |
| `land`, `jump` | a hard landing / a jump | |

Everything that reads the screen (`app`, `title`, `windows`, `fullscreen`, `window-opened`,
`window-closed`) is switched off by the user's "Comment on what is on screen" setting.
Placeholders: `{user}` `{app}` `{title}` `{n}` `{pet}` `{font}` `{theme}` `{time}` `{hour}` `{res}` `{monitors}`.

## Your own comments

Users can add to or replace any pet's lines without touching the pack: create
`~/.local/share/deskpals/comments/<pack id>.json` (the settings window has an "Edit comments..."
button that makes it for you). It is picked up as soon as it is saved.

```json
{
  "mode": "add",
  "lines": ["A new random remark"],
  "topics": {"pickup": ["Hands off, {user}!"]},
  "interval": [10, 30]
}
```

`mode` `"add"` (default) appends to the pack's lines; `"replace"` swaps every topic you list for
yours. Bad lines are skipped and logged, never fatal.

## Making frames

Anything that writes PNGs works. The bundled packs are drawn with small Python scripts in each
pack's `_src/` folder (pixel by pixel, then scaled up 2x with no smoothing) - copy one as a starting
point.
