# Custom Pets Guide

# Custom Pets Guide

**You can add your own collector pets, combat pets and mounts without touching any code. Each pet type is one JSON file in the plugin folder.**

---

## Where the files are

The plugin keeps its files in `mods/kyuubisoft_pets/` on the server:

```
mods/kyuubisoft_pets/
├── config.json
├── stat-pool.json             stats, rarity rules, combat defaults
├── loot_luck_blacklist.json
├── pets.db                    pet database (SQLite)
├── npc-roles/                 NPC roles, written on every server start
└── pet-types/
    ├── collector/             26 collector pets + _template.json
    ├── combat/                13 combat pets + _template.json
    └── mount/                 9 mounts + _template.json

mods/KyuubiSoftPets_CustomItems/   items generated for your custom pets
```

How the plugin reads `pet-types/`:

- Every `.json` file below `pet-types/` is loaded, also in further subfolders. Files whose name starts with `_` are skipped; that is why the templates are called `_template.json`.
- The module comes from the `module` field. Without it, the folder decides: `combat/` and `mount/` give that module, every other folder gives a collector pet.
- JSON files placed directly in `pet-types/` are moved into the folder of their module on the next load.
- The shipped files are only written when they are missing. Your changes to them survive plugin updates, but a deleted shipped file comes back on the next start or reload, so change a shipped pet by editing its file.
- Every `id` must be unique across all folders. With duplicates the file that loads last (alphabetical by path) wins.

---

## Quick start

1. **Copy** the `_template.json` of the module you want (for example `pet-types/collector/_template.json`) and give the copy a new name, for example `phoenix.json`.
2. **Edit** the copy. The fields are explained below.
3. **Restart the server.** The log shows `Loaded: collector/phoenix rarity=epic (item=Pet_Egg_Phoenix, file=...)`. Because no item `Pet_Egg_Phoenix` exists yet, the plugin writes one and logs `Generated item 'Pet_Egg_Phoenix' for custom pet 'phoenix'` and `Restart the server once more to load the new items.`
4. **Restart again.** Hytale now loads the new item.
5. Hand the pet out with `/kspetadmin give <player> phoenix`, in the admin window ("Create pet", see [Admin Panel](./admin-panel)), or as a blank egg (`/give`, shops, loot); a blank egg hatches on its first right-click.

:::note[Why two restarts]
Hytale loads its items before plugins start. On the first start the plugin can only write the item file; Hytale reads it on the next start. A pet type that uses an item which already exists works after a single restart or even a `/kspetadmin reload`.
:::

---

## Fields for every pet type

Defaults are the values the plugin uses when a field is left out. The templates set most fields explicitly.

| Field | Type | Default | Meaning |
|-------|------|---------|---------|
| `id` | text | (required) | Unique ID, used in commands and as the base of the item ID. Use lowercase letters, digits and `_`; commands lowercase what you type |
| `displayName` | text | (empty: the ID is shown) | Name shown in the compendium, the admin window and generated item names; new pets get it as their name |
| `entityTypeId` | text | (required) | Model to use, by model name, for example `Fox`, `Wolf_Black`, `Dragon_Frost` |
| `modelScale` | number | 0.8 | Size of the model in the world (0.5 = half size) |
| `maxLevel` | number | 50 | Highest level |
| `baseXpPerLevel` | number | 100 | XP needed for level 2 |
| `xpScaleFactor` | number | 1.15 | Growth of the XP requirement per level (1.15 = 15 % more each level) |
| `rarity` | text | `common` | `uncommon`, `rare`, `epic` or `legendary` (lowercase); see [Rarity effects](#rarity-effects) |
| `description` | text | empty | Shown in the compendium for players who do not own the pet |
| `obtainHint` | text | empty | Since 1.9.0: text under "How to get it" in the compendium, for players who do not own the pet. Empty shows "Ask the server team how to get this pet." |
| `previewScale` | number | 0 | Since 1.9.0: extra size factor for the compendium's 3D stage (0 = automatic only) |
| `module` | text | from the folder | `collector`, `combat` or `mount` |

The XP needed from level L to L + 1 is `baseXpPerLevel * xpScaleFactor ^ (L - 1)`.

### Collector pets

| Field | Type | Default | Meaning |
|-------|------|---------|---------|
| `pickupRadius` | number | 5.0 | Distance in blocks at which the pet grabs an item; the Pickup Range stat adds to it. The shipped pets use 1.5 to 2.5 |
| `eggItemId` | text | `Pet_Egg_<Id>` | Item that summons the pet (see [Summon items](#summon-items)) |

### Combat pets

| Field | Type | Default | Meaning |
|-------|------|---------|---------|
| `runeItemId` | text | `Battle_Rune_<Id>` | Item that summons the pet |
| `combatStats.baseDamage` | number | 8.0 | Base damage; the pet hits for `(baseDamage + level * damagePerLevel) * (1 + Damage stat)` |
| `combatStats.damagePerLevel` | number | 0.5 | Damage added per level |
| `combatStats.attackSpeed` | number | 1.5 | Attacks per second |
| `combatStats.attackRange` | number | 3.0 | Distance in blocks from which the pet hits |
| `combatStats.aggroRange` | number | 15.0 | Shown in the stats; not used for targeting |
| `combatStats.leashDistance` | number | 20.0 | Maximum distance between pet and a new target |
| `combatStats.attackAnimation` | text | `Bite` | Animation played on each hit (`Bite`, `Ram`, `SwingLeft`, ...); an empty value plays `Attack` |
| `combatStats.attackAnimationsId` | text | empty | Animation set of the model, for example `Wolf_Black_Default`; empty uses `<entityTypeId>_Default` |
| `combatStats.baseHealth` | number | 0 | HP at level 1 when combat pet mortality is on; 0 = use `combatBalance.mortality.baseHealth` |
| `combatStats.healthPerLevel` | number | -1 | HP per level above 1 with mortality on; -1 = use `combatBalance.mortality.healthPerLevel` |

:::note[combatDefaults]
The first six `combatStats` fields fall back to `combatDefaults` in `stat-pool.json` when they are left out or set to exactly the default shown here. With the shipped `combatDefaults` this changes nothing; if you change `combatDefaults`, every pet type that uses one of these exact default values follows it.
:::

Combat pets need the combat module (`modules.combat`); their NPC role is only loaded while it is on. How combat pets behave: [Combat Pets](./combat-pets).

### Mounts

| Field | Type | Default | Meaning |
|-------|------|---------|---------|
| `hornItemId` | text | `Mount_Horn_<Id>` | Item that summons the mount |
| `mountConfig.mountType` | text | `walk` | `walk`, `fly` or `swim` |
| `mountConfig.mountYOffset` | number | 2.5 | Seat height of the rider above the mount, in blocks |
| `mountConfig.speed` | number | 1.5 | Movement speed while riding (1.0 = normal) |
| `mountConfig.baseHealth` | number | 200 | Added to the mount's health; mounts are invulnerable, so it has no effect in 1.12.1 |

A speed other than 1.0 needs a speed role (`KS_Mount_Walk_S14_Role` for 1.4). Walk roles for 1.1 to 3.0 and the fly role for 1.8 ship with the plugin; any other speed is generated on server start. Restart the server after adding a mount with a new speed. How mounts behave: [Mounts](./mounts).

### Advanced and legacy fields

| Field | Meaning |
|-------|---------|
| `npcRoleId` | NPC role to use instead of the default (`KS_Pet_Follow_Role` for collector pets, `KS_Pet_Combat_Role` for combat pets, the `KS_Mount_*` roles for mounts). If the role does not exist, a collector or combat pet falls back to `KS_Pet_Follow_Role`; a mount is not summoned. A collector pet with a role that lacks the shipped role's leash instruction still follows you but does not fetch items |
| `followDistance`, `followSpeed` | Still in the shipped files, but not read in 1.12.1; the NPC role decides how the pet follows |
| `baseBuffs`, `buffsPerLevel` | Fixed buffs from older versions, only used for pets that have no rolled stats. An `xp_bonus` entry in them is always added to the pet's XP gain |
| `abilities` | Read from the file, but not used |

Fields starting with `_` (such as the templates' `_comment`) are ignored.

---

## Summon items

- If you leave the item ID out, it is built from the pet's ID with every part capitalised: `baby_phoenix` becomes `Pet_Egg_Baby_Phoenix`, `Battle_Rune_Baby_Phoenix` or `Mount_Horn_Baby_Phoenix`.
- On every start and reload the plugin checks each pet type's item. If no item with that ID exists (not in the jar, not in any loaded asset pack, not generated before), it writes one to `mods/KyuubiSoftPets_CustomItems/Server/Item/Items/<ItemId>.json` and creates that folder's `manifest.json` ("Custom Pet Items"). Hytale loads it on the next start.
- The generated item uses the plugin's egg, rune or horn model with the texture of its rarity, is called `<displayName> Egg`, `Battle Rune: <displayName>` or `Mount Horn: <displayName>`, does not stack and is not dropped on death. You can edit the generated file; the plugin does not overwrite an existing one.
- You can also point the item ID at an item from your own asset pack. It needs the plugin's interaction on secondary use, or right-clicking it does nothing:

```json
"Interactions": {
  "Secondary": {
    "Interactions": [ { "Type": "pet_egg_use" } ]
  }
}
```

Use `battle_rune_use` for combat pets and `mount_horn_use` for mounts.

---

## Rarity effects

| What | Effect |
|------|--------|
| Stats | Number of stats, unlock levels and roll quality from `rarityRules` in `stat-pool.json` (uncommon 2, rare 3, epic 4, legendary 5 stats; details in [Collector Pets](./collector-pets)) |
| Item quality | The summon item's quality is set to the rarity on every start and reload (`uncommon`, `rare`, `epic`, `legendary`; anything else becomes Common), which colours the item name |
| Generated item | Uses the egg, rune or horn texture of the rarity (Uncommon for any other value) |
| Compendium | Pets are sorted by rarity (legendary first); names and rings show the rarity colour, also on the HUD |
| First hatch | Hatching the first egg of a collector pet type shows the "New Pet Discovered!" title with the pet name in the rarity colour |

:::caution[common]
`common` is the default, but the shipped `stat-pool.json` has no rule for it. A pet with `common` (or any rarity without a rule) gets one stat at level 1 with a roll quality between 0.5 and 0.8 and never rolls more stats. Use one of the four shipped rarities, or add a rule for your rarity to `rarityRules`.
:::

---

## Compendium fields

- `obtainHint` and `description` are plain text from the file; they are not translated. Players who own the pet see its stats instead.
- The 3D stage scales every model to a common size from its hitbox. `previewScale` multiplies on top of that: 1.5 shows the model larger, 0.7 smaller, 0 leaves it automatic.
- Whether players see pets they do not own is set with `ui.lockedPets` in `config.json` (see [Configuration](./configuration) and [UI Pages](./ui-pages)).

---

## Examples

### Collector pet

```json
{
  "id": "phoenix",
  "displayName": "Baby Phoenix",
  "entityTypeId": "Skrill_Chick",
  "modelScale": 0.6,
  "pickupRadius": 2.0,
  "maxLevel": 50,
  "baseXpPerLevel": 120,
  "xpScaleFactor": 1.18,
  "eggItemId": "Pet_Egg_Phoenix",
  "rarity": "epic",
  "description": "A fiery phoenix chick that collects items.",
  "obtainHint": "Sold by the pet trader at spawn.",
  "previewScale": 0
}
```

### Combat pet

```json
{
  "id": "fire_golem_combat",
  "displayName": "Fire Golem",
  "entityTypeId": "Bramblekin",
  "modelScale": 1.2,
  "maxLevel": 50,
  "baseXpPerLevel": 140,
  "xpScaleFactor": 1.15,
  "rarity": "epic",
  "description": "A burning golem that fights by your side.",
  "obtainHint": "Boss drop.",
  "previewScale": 0,
  "module": "combat",
  "runeItemId": "Battle_Rune_Fire_Golem",
  "combatStats": {
    "baseDamage": 12.0,
    "damagePerLevel": 0.7,
    "attackSpeed": 1.0,
    "attackRange": 3.0,
    "aggroRange": 15.0,
    "leashDistance": 20.0,
    "attackAnimation": "Bite",
    "attackAnimationsId": "Bramblekin_Default",
    "baseHealth": 0,
    "healthPerLevel": -1
  }
}
```

### Mount

```json
{
  "id": "war_bison_mount",
  "displayName": "War Bison",
  "entityTypeId": "Bison",
  "modelScale": 1.3,
  "maxLevel": 30,
  "baseXpPerLevel": 180,
  "xpScaleFactor": 1.2,
  "rarity": "legendary",
  "description": "A massive war bison. Slow but unstoppable.",
  "obtainHint": "",
  "previewScale": 0,
  "module": "mount",
  "hornItemId": "Mount_Horn_War_Bison",
  "mountConfig": {
    "mountType": "walk",
    "mountYOffset": 2.8,
    "speed": 1.2,
    "baseHealth": 200
  }
}
```

---

## Models

`entityTypeId` takes any model name of the game; write it exactly as the model is named. The shipped pets use these:

- **Collector pets:** `Archaeopteryx`, `Bat`, `Bluebird`, `Bunny`, `Cactee`, `Cat`, `Crab`, `Dragon_Fire`, `Dragon_Frost`, `Duck`, `Fen_Stalker`, `Feran_Cube`, `Fox`, `Frog_Green`, `Gecko`, `Hatworm`, `Lizard_Sand`, `Meerkat`, `Owl_Snow`, `Parrot`, `Penguin`, `Skrill_Chick`, `Squirrel`, `Trork`, `Wolf_Black`, `Yeti`
- **Combat pets:** `Boar`, `Bramblekin`, `Crawler_Void`, `Crocodile`, `Hyena`, `Lizard_Sand`, `Rex_Cave`, `Scorpion`, `Shadow_Knight`, `Spider`, `Wolf_Black`, `Wolf_Outlander_Priest`, `Yeti`
- **Mounts:** `Bear_Polar`, `Bison`, `Camel`, `Dragon_Frost`, `Horse`, `Moose_Bull`, `Rex_Cave`, `Tiger_Sabertooth`, `Wolf_Black`

Small `modelScale` values make baby versions of big creatures (the shipped Fire Dragon uses 0.16).

---

## Reload or restart

| Change | What you need |
|--------|---------------|
| Values of an existing pet type (names, texts, rarity, XP curve, pickup radius, combat stats) | `/kspetadmin reload` or "Reload" in the admin window |
| A new pet type whose item already exists | `/kspetadmin reload` |
| A new pet type that needs a new item | Two restarts |
| A mount speed whose speed role does not exist yet | One restart |

`/kspetadmin reload` reloads `config.json`, the pet types, `stat-pool.json`, the loot luck blacklist and the language files. Pets that already exist keep their rolled stats; new rolls use the new rules. Stats outside the new ranges are clamped when the owner joins next time.

---

## Troubleshooting

**The pet type is missing in `/kspetadmin list`**
- Check the folder (`mods/kyuubisoft_pets/pet-types/...`) and that the file name does not start with `_`.
- Check the JSON syntax (no trailing commas). The log shows `Failed to load <file>: ...` or `Invalid pet type in <file>: missing id`.

**No item was generated**
- Look for `Generated item '...'` in the log and in `mods/KyuubiSoftPets_CustomItems/Server/Item/Items/`.
- No file is generated when an item with that ID already exists somewhere (jar, another asset pack). Then that item is used.

**Right-clicking the item does nothing**
- The log line `Pet egg interaction: no pet type for item '<id>' — registered eggs: [...]` means the item ID in the pet type does not match the item.
- Items from your own asset pack need the interaction shown in [Summon items](#summon-items).
- Battle Runes and Mount Horns do nothing while their module is off.

**The pet does not appear**
- `Invalid entityTypeId for pet: <name>`: the model name is wrong.
- `Invalid entityTypeId for mount: <name>`: the same for a mount.
- `NPC Role not found: KS_Pet_Follow_Role — cannot spawn pet <id>`: the plugin's NPC roles did not load; check the start of the server log.
- `Mount role not found: <role>`: the speed role of a new mount speed (or the mount's `npcRoleId`) is missing; restart the server after a speed change.

**The collector pet does not fetch items**
- Keep `pickupRadius` at 1.5 or more. The shipped follow role stops the pet about one block from the item, so a smaller radius may never reach it.
- A custom `npcRoleId` without the shipped role's leash instruction follows but never fetches.

**The pet is too big or too small**
- `modelScale` sizes the pet in the world, `previewScale` only in the compendium.

Useful admin commands while testing: `/kspetadmin list`, `/kspetadmin give <player> <type>`, `/kspetadmin reload` and `/kspetadmin inspect hand`. Full syntax: [Commands](./commands).