pet.trezy.species

trezy.pet

{
  "id": "pet.trezy.species",
  "defs": {
    "main": {
      "key": "any",
      "type": "record",
      "record": {
        "type": "object",
        "required": [
          "id",
          "displayName",
          "tier",
          "sprite",
          "behaviors"
        ],
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 64,
            "description": "The species key. Stored on every ledger row already sold, so renaming one is not safe — a rename is a new species plus a migration, never an edit."
          },
          "tier": {
            "type": "string",
            "maxLength": 64,
            "description": "The id of a pet.trezy.tier record, which carries the price. Deliberately NOT an enum: adding a tier must be publishing one record, not editing this file, regenerating and redeploying the client. knownValues documents the tiers that exist today and constrains nothing.",
            "knownValues": [
              "common",
              "uncommon",
              "rare",
              "epic",
              "legendary"
            ]
          },
          "price": {
            "type": "integer",
            "minimum": 0,
            "description": "Overrides the tier's price for this species alone. Absent is the normal case — an override per species would mean the tiers are wrong."
          },
          "groups": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 64
            },
            "description": "The `pet.trezy.group` records this species belongs to, by id. AN ARRAY BECAUSE MEMBERSHIP OVERLAPS AND MIXES LEVELS: a bat is `mammals`, `bats`, `fliers` and `nocturnal`, and a penguin is `birds` and `swimmers` without being a mammal at all. A single field would have forced a species to pick one true thing about itself and drop the others — which is why the vocabulary can hold a class, a family and a habit side by side rather than having to settle on one rank. Order is not significant; readers sort by the group's own `rank`."
          },
          "sprite": {
            "ref": "#sprite",
            "type": "ref",
            "description": "Everything needed to draw the animal, including the atlas itself as a blob. Generated from the artist's Aseprite export by scripts/build-species.ts and never hand-edited."
          },
          "activity": {
            "type": "string",
            "maxLength": 64,
            "description": "When the animal keeps its hours. ⚠ A FIELD RATHER THAN A GROUP, BECAUSE THESE ARE MUTUALLY EXCLUSIVE AND GROUPS ARE NOT. `groups` overlap on purpose — a bat is a mammal and a flier at once — so nothing there can enforce exactly-one, and a species tagged both `nocturnal` and `diurnal` would be nonsense no reader could resolve. It also has to be exactly one for the mechanic that will read it: a pet dozes outside its own hours, and 'outside' is undefined for an animal with two answers.\n\nThe vocabulary, widest first: `diurnal` is daylight; `nocturnal` is night; `crepuscular` is both twilights; `matutinal` is dawn ONLY and `vespertinal` is dusk ONLY, the two halves crepuscular rolls together; `cathemeral` is no fixed rhythm, which is the honest answer for a slime.\n\n⚠ `matutinal` AND `vespertinal` ARE PUBLISHED AHEAD OF ANY USER, DELIBERATELY. No species on the roster is either one today. They are listed so the vocabulary is settled before somebody needs it — a reader implementing dozing should handle all six now rather than discover two of them later. NOT an enum, for the same reason `tier` is not: the list documents what exists and constrains nothing, so a seventh is a record edit rather than a client deploy.\n\n(Spelled `vespertinal` to match the -al pattern of the others; `vespertine` is the commoner form in the literature and means the same thing. Do not 'correct' one to the other — the string is stored on records.)",
            "knownValues": [
              "diurnal",
              "nocturnal",
              "crepuscular",
              "matutinal",
              "vespertinal",
              "cathemeral"
            ]
          },
          "behaviors": {
            "type": "array",
            "items": {
              "ref": "#behavior",
              "type": "ref"
            },
            "minLength": 1,
            "description": "How the species acts when left alone, as weighted options for the behaviour tree. Authored, unlike `sprite` — this is temperament, a game-design judgement that no export contains."
          },
          "displayName": {
            "type": "string",
            "maxLength": 64
          },
          "purchasable": {
            "type": "boolean",
            "description": "ABSENT MEANS TRUE. False means the species exists in every other sense — tier, sprite, ownable, on the leaderboard — but the shop will not sell it because it is reached another way. A reader writing `if purchasable then` makes every species unsellable."
          }
        }
      },
      "description": "A species the game knows about, published in the game's own repo (did:plc:ollch4g7tes2reb5eceiwuol). Only records from that DID are ever read — record.index:pet.trezy.species refuses the rest. THIS RECORD IS THE WHOLE SPECIES: identity, economy, artwork and temperament. The client compiles in nothing about any species and derives nothing from a filename; it draws what these records say and caches the answer. A species not published here does not exist to the game, and changing one here changes it for every client without an app deploy."
    },
    "rect": {
      "type": "object",
      "required": [
        "x",
        "y",
        "w",
        "h"
      ],
      "properties": {
        "h": {
          "type": "integer",
          "minimum": 0
        },
        "w": {
          "type": "integer",
          "minimum": 0
        },
        "x": {
          "type": "integer"
        },
        "y": {
          "type": "integer"
        }
      }
    },
    "size": {
      "type": "object",
      "required": [
        "w",
        "h"
      ],
      "properties": {
        "h": {
          "type": "integer",
          "minimum": 1
        },
        "w": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "datum": {
      "type": "object",
      "required": [
        "key",
        "value"
      ],
      "properties": {
        "key": {
          "type": "string",
          "maxLength": 64
        },
        "value": {
          "type": "string",
          "maxLength": 256
        }
      }
    },
    "frame": {
      "type": "object",
      "required": [
        "x",
        "y",
        "w",
        "h",
        "duration"
      ],
      "properties": {
        "h": {
          "type": "integer",
          "minimum": 1
        },
        "w": {
          "type": "integer",
          "minimum": 1
        },
        "x": {
          "type": "integer",
          "minimum": 0
        },
        "y": {
          "type": "integer",
          "minimum": 0
        },
        "duration": {
          "type": "integer",
          "minimum": 1,
          "description": "Milliseconds, as Aseprite exports it."
        }
      },
      "description": "One frame: where it sits in the atlas, and how long it is held."
    },
    "slice": {
      "type": "object",
      "required": [
        "name",
        "bounds"
      ],
      "properties": {
        "data": {
          "type": "array",
          "items": {
            "ref": "#datum",
            "type": "ref"
          },
          "description": "The slice's user-data, as key/value pairs. Aseprite stores this as a free-text string like `type=ground-contact`; it is split at build time so no reader has to. `type` is the only key the game reads today, and unknown keys are carried through rather than dropped."
        },
        "name": {
          "type": "string",
          "maxLength": 64
        },
        "bounds": {
          "ref": "#rect",
          "type": "ref"
        }
      }
    },
    "sprite": {
      "type": "object",
      "required": [
        "atlas",
        "size",
        "frameSize",
        "animations",
        "slices"
      ],
      "properties": {
        "size": {
          "ref": "#size",
          "type": "ref",
          "description": "The atlas's own pixel dimensions. Needed to place background-position offsets without waiting for the image to load."
        },
        "atlas": {
          "type": "blob",
          "accept": [
            "image/png"
          ],
          "maxSize": 1000000,
          "description": "The packed spritesheet PNG, stored in this repo. A blob rather than a URL into the app's public directory, so the artwork and the frame rectangles that index into it version together and CANNOT disagree — a repacked atlas with stale coordinates draws slivers of the wrong animal, which is the exact drift this record exists to end. Pixel-art atlases are a few KB; the cap is generous by three orders of magnitude and is only there to catch a mistake."
        },
        "slices": {
          "type": "array",
          "items": {
            "ref": "#slice",
            "type": "ref"
          },
          "description": "Authored regions: the hitbox a pointer can grab, and the ground-contact box that decides where the animal's feet are. Absent or unlabelled slices fall back to a measured box, so this may be empty."
        },
        "frameSize": {
          "ref": "#size",
          "type": "ref",
          "description": "One frame's dimensions — the window the animal is drawn inside, typically 64×64 with the creature somewhere within it rather than filling it."
        },
        "animations": {
          "type": "array",
          "items": {
            "ref": "#animation",
            "type": "ref"
          },
          "minLength": 1,
          "description": "Named animations. AN ARRAY, NOT A MAP, because lexicon has no map type and an `unknown` blob here would put this back to being parsed by hand at both ends. The client keys it by name on read."
        }
      },
      "description": "The drawable half of a species. Mirrors the client's SpriteSheet shape exactly so the client does no parsing: it reads these fields straight into the renderer. The Aseprite JSON is NOT carried verbatim — that format numbers frames per tag and stores tag membership as index ranges into an insertion-ordered object, which has caused two separate animation bugs in this project. The ranges are resolved here, once, at build time."
    },
    "behavior": {
      "type": "object",
      "required": [
        "type",
        "weight"
      ],
      "properties": {
        "type": {
          "type": "string",
          "maxLength": 32,
          "description": "NOT an enum, for the same reason `tier` is not: a new behaviour should be publishable. A client that meets a type it does not implement must skip that option and keep the rest, never refuse the species.",
          "knownValues": [
            "idle",
            "move",
            "slide",
            "jump"
          ]
        },
        "speed": {
          "type": "integer",
          "minimum": 0,
          "description": "World pixels per second. Characterisation rather than realism — the turtle is slow because a slow turtle is funny, and the spread across species is what makes a mixed stage read as a menagerie."
        },
        "weight": {
          "type": "integer",
          "minimum": 0,
          "description": "Relative likelihood against the species's other options. Zero is a legitimate way to keep a behaviour published but dormant."
        },
        "animation": {
          "type": "string",
          "maxLength": 64,
          "description": "For `idle` and `move`: the animation to play throughout."
        },
        "maxDuration": {
          "type": "integer",
          "minimum": 0,
          "description": "For `idle`: seconds, the longest."
        },
        "minDuration": {
          "type": "integer",
          "minimum": 0,
          "description": "For `idle`: seconds, the shortest the pet will stand still."
        },
        "endAnimation": {
          "type": "string",
          "maxLength": 64,
          "description": "For `slide` and `jump`: the recovery, played once."
        },
        "endMoveFrame": {
          "type": "integer",
          "minimum": 0,
          "description": "For `jump`: the frame of the end animation at which it lands."
        },
        "loopAnimation": {
          "type": "string",
          "maxLength": 64,
          "description": "For `slide` and `jump`: the sustained middle, repeated while travelling."
        },
        "startAnimation": {
          "type": "string",
          "maxLength": 64,
          "description": "For `slide` and `jump`: the wind-up, played once."
        },
        "startMoveFrame": {
          "type": "integer",
          "minimum": 0,
          "description": "For `jump`: the frame of the start animation at which the pet actually leaves the ground, so the launch matches the drawing."
        }
      },
      "description": "One weighted option in the behaviour tree. The params a type needs differ by type, which lexicon cannot express as a discriminated union, so every param is optional here and build-species.ts validates the combination that each type actually requires."
    },
    "animation": {
      "type": "object",
      "required": [
        "name",
        "frames"
      ],
      "properties": {
        "name": {
          "type": "string",
          "maxLength": 64,
          "description": "The tag name from Aseprite — `idle`, `run`, `slide-loop`. Behaviours reference these by name, and a behaviour naming an animation that is not here does not throw: the pet simply stops animating. build-species.ts refuses to emit that combination."
        },
        "frames": {
          "type": "array",
          "items": {
            "ref": "#frame",
            "type": "ref"
          },
          "minLength": 1
        }
      }
    }
  },
  "$type": "com.atproto.lexicon.schema",
  "lexicon": 1
}

Validate Record

Validate a record against pet.trezy.species

Validation Options
Treat any remaining unresolved references as valid

Metadata

DID
did:plc:ollch4g7tes2reb5eceiwuol
CID
bafyreigtodk3l27igp4336sqq3wusflrpni5cv2ljwe5kbyblkzy4t5nxe
Indexed At
2026-08-09 01:42 UTC
AT-URI
at://did:plc:ollch4g7tes2reb5eceiwuol/com.atproto.lexicon.schema/pet.trezy.species

Version History (3 versions)

Lexicon Garden

@