pet.trezy.species

trezy.pet

Schema Diff

+10 -0

From

CID
bafyreihycy5b3z7...
Indexed At
2026-08-10 13:18 UTC
View this version

To

CID
bafyreidlqc4j4om...
Indexed At
2026-09-01 15:54 UTC
View this version

Compatibility Analysis

Backward Compatible

Backward compatible. 6 non-breaking changes.

Non-Breaking Changes (6)
  • AddedVertex AddedVertex { vertex_id: "pet.trezy.species#sprite.box" }
  • AddedVertex AddedVertex { vertex_id: "pet.trezy.species#sprite.portrait" }
  • AddedEdge AddedEdge { src: "pet.trezy.species#sprite", tgt: "pet.trezy.species#sprite.box", kind: "prop", name: Some("box") }
  • AddedEdge AddedEdge { src: "pet.trezy.species#sprite", tgt: "pet.trezy.species#sprite.portrait", kind: "prop", name: Some("portrait") }
  • AddedEdge AddedEdge { src: "pet.trezy.species#sprite.box", tgt: "pet.trezy.species#size", kind: "ref", name: None }
  • AddedEdge AddedEdge { src: "pet.trezy.species#sprite.portrait", tgt: "pet.trezy.species#size", kind: "ref", name: None }

Migration Guidance

Added Elements

  • AddedVertex { vertex_id: "pet.trezy.species#sprite.box" }
  • AddedVertex { vertex_id: "pet.trezy.species#sprite.portrait" }

Additional Notes

  • Non-breaking: AddedEdge { src: "pet.trezy.species#sprite", tgt: "pet.trezy.species#sprite.box", kind: "prop", name: Some("box") }
  • Non-breaking: AddedEdge { src: "pet.trezy.species#sprite", tgt: "pet.trezy.species#sprite.portrait", kind: "prop", name: Some("portrait") }
  • Non-breaking: AddedEdge { src: "pet.trezy.species#sprite.box", tgt: "pet.trezy.species#size", kind: "ref", name: None }
  • Non-breaking: AddedEdge { src: "pet.trezy.species#sprite.portrait", tgt: "pet.trezy.species#size", kind: "ref", name: None }
1 1
{
2 2
  "$type": "com.atproto.lexicon.schema",
3 3
  "defs": {
4 4
    "animation": {
5 5
      "properties": {
6 6
        "frames": {
7 7
          "items": {
8 8
            "ref": "#frame",
9 9
            "type": "ref"
10 10
          },
11 11
          "minLength": 1,
12 12
          "type": "array"
13 13
        },
14 14
        "name": {
15 15
          "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.",
16 16
          "maxLength": 64,
17 17
          "type": "string"
18 18
        }
19 19
      },
20 20
      "required": [
21 21
        "name",
22 22
        "frames"
23 23
      ],
24 24
      "type": "object"
25 25
    },
26 26
    "behavior": {
27 27
      "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.",
28 28
      "properties": {
29 29
        "animation": {
30 30
          "description": "For `idle` and `move`: the animation to play throughout.",
31 31
          "maxLength": 64,
32 32
          "type": "string"
33 33
        },
34 34
        "endAnimation": {
35 35
          "description": "For `slide` and `jump`: the recovery, played once.",
36 36
          "maxLength": 64,
37 37
          "type": "string"
38 38
        },
39 39
        "endMoveFrame": {
40 40
          "description": "For `jump`: the frame of the end animation at which it lands.",
41 41
          "minimum": 0,
42 42
          "type": "integer"
43 43
        },
44 44
        "hoverAnimation": {
45 45
          "description": "For `fly`: held on the spot after arriving somewhere, before it picks the next destination or comes down. Optional — a species without one simply turns and goes again.",
46 46
          "maxLength": 64,
47 47
          "type": "string"
48 48
        },
49 49
        "loopAnimation": {
50 50
          "description": "For `slide` and `jump`: the sustained middle, repeated while travelling.",
51 51
          "maxLength": 64,
52 52
          "type": "string"
53 53
        },
54 54
        "maxDuration": {
55 55
          "description": "For `idle`: seconds, the longest. For `fly`: the longest it stays up.",
56 56
          "minimum": 0,
57 57
          "type": "integer"
58 58
        },
59 59
        "minDuration": {
60 60
          "description": "For `idle`: seconds, the shortest the pet will stand still. For `fly`: seconds aloft before it looks for somewhere to land.",
61 61
          "minimum": 0,
62 62
          "type": "integer"
63 63
        },
64 64
        "speed": {
65 65
          "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.",
66 66
          "minimum": 0,
67 67
          "type": "integer"
68 68
        },
69 69
        "startAnimation": {
70 70
          "description": "For `slide` and `jump`: the wind-up, played once.",
71 71
          "maxLength": 64,
72 72
          "type": "string"
73 73
        },
74 74
        "startMoveFrame": {
75 75
          "description": "For `jump`: the frame of the start animation at which the pet actually leaves the ground, so the launch matches the drawing.",
76 76
          "minimum": 0,
77 77
          "type": "integer"
78 78
        },
79 79
        "type": {
80 80
          "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.",
81 81
          "knownValues": [
82 82
            "idle",
83 83
            "move",
84 84
            "slide",
85 85
            "jump",
86 86
            "fly"
87 87
          ],
88 88
          "maxLength": 32,
89 89
          "type": "string"
90 90
        },
91 91
        "weight": {
92 92
          "description": "Relative likelihood against the species's other options. Zero is a legitimate way to keep a behaviour published but dormant.",
93 93
          "minimum": 0,
94 94
          "type": "integer"
95 95
        }
96 96
      },
97 97
      "required": [
98 98
        "type",
99 99
        "weight"
100 100
      ],
101 101
      "type": "object"
102 102
    },
103 103
    "datum": {
104 104
      "properties": {
105 105
        "key": {
106 106
          "maxLength": 64,
107 107
          "type": "string"
108 108
        },
109 109
        "value": {
110 110
          "maxLength": 256,
111 111
          "type": "string"
112 112
        }
113 113
      },
114 114
      "required": [
115 115
        "key",
116 116
        "value"
117 117
      ],
118 118
      "type": "object"
119 119
    },
120 120
    "frame": {
121 121
      "description": "One frame: where it sits in the atlas, and how long it is held.",
122 122
      "properties": {
123 123
        "duration": {
124 124
          "description": "Milliseconds, as Aseprite exports it.",
125 125
          "minimum": 1,
126 126
          "type": "integer"
127 127
        },
128 128
        "h": {
129 129
          "minimum": 1,
130 130
          "type": "integer"
131 131
        },
132 132
        "w": {
133 133
          "minimum": 1,
134 134
          "type": "integer"
135 135
        },
136 136
        "x": {
137 137
          "minimum": 0,
138 138
          "type": "integer"
139 139
        },
140 140
        "y": {
141 141
          "minimum": 0,
142 142
          "type": "integer"
143 143
        }
144 144
      },
145 145
      "required": [
146 146
        "x",
147 147
        "y",
148 148
        "w",
149 149
        "h",
150 150
        "duration"
151 151
      ],
152 152
      "type": "object"
153 153
    },
154 154
    "main": {
155 155
      "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.",
156 156
      "key": "any",
157 157
      "record": {
158 158
        "properties": {
159 159
          "activity": {
160 160
            "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.)",
161 161
            "knownValues": [
162 162
              "diurnal",
163 163
              "nocturnal",
164 164
              "crepuscular",
165 165
              "matutinal",
166 166
              "vespertinal",
167 167
              "cathemeral"
168 168
            ],
169 169
            "maxLength": 64,
170 170
            "type": "string"
171 171
          },
172 172
          "behaviors": {
173 173
            "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.",
174 174
            "items": {
175 175
              "ref": "#behavior",
176 176
              "type": "ref"
177 177
            },
178 178
            "minLength": 1,
179 179
            "type": "array"
180 180
          },
181 181
          "displayName": {
182 182
            "maxLength": 64,
183 183
            "type": "string"
184 184
          },
185 185
          "groups": {
186 186
            "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`.",
187 187
            "items": {
188 188
              "maxLength": 64,
189 189
              "type": "string"
190 190
            },
191 191
            "type": "array"
192 192
          },
193 193
          "id": {
194 194
            "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.",
195 195
            "maxLength": 64,
196 196
            "type": "string"
197 197
          },
198 198
          "price": {
199 199
            "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.",
200 200
            "minimum": 0,
201 201
            "type": "integer"
202 202
          },
203 203
          "purchasable": {
204 204
            "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.",
205 205
            "type": "boolean"
206 206
          },
207 207
          "sprite": {
208 208
            "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.",
209 209
            "ref": "#sprite",
210 210
            "type": "ref"
211 211
          },
212 212
          "starter": {
213 213
            "description": "Offered as a first pet. ABSENT MEANS NO, and the flag is a whitelist rather than a hint: when any species carries it, the starter picker offers exactly those and the price rule is not consulted. When none does, the picker falls back to everything the starter grant can afford, which is what it did before this field existed. Kept here rather than as a list on gameConfig because it is a fact about the species, and because a full species record already travels to every client in getDefinitions — a list elsewhere would need the query, its output lexicon and its Lua changed to say the same thing.",
214 214
            "type": "boolean"
215 215
          },
216 216
          "tier": {
217 217
            "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.",
218 218
            "knownValues": [
219 219
              "common",
220 220
              "uncommon",
221 221
              "rare",
222 222
              "epic",
223 223
              "legendary"
224 224
            ],
225 225
            "maxLength": 64,
226 226
            "type": "string"
227 227
          }
228 228
        },
229 229
        "required": [
230 230
          "id",
231 231
          "displayName",
232 232
          "tier",
233 233
          "sprite",
234 234
          "behaviors"
235 235
        ],
236 236
        "type": "object"
237 237
      },
238 238
      "type": "record"
239 239
    },
240 240
    "rect": {
241 241
      "properties": {
242 242
        "h": {
243 243
          "minimum": 0,
244 244
          "type": "integer"
245 245
        },
246 246
        "w": {
247 247
          "minimum": 0,
248 248
          "type": "integer"
249 249
        },
250 250
        "x": {
251 251
          "type": "integer"
252 252
        },
253 253
        "y": {
254 254
          "type": "integer"
255 255
        }
256 256
      },
257 257
      "required": [
258 258
        "x",
259 259
        "y",
260 260
        "w",
261 261
        "h"
262 262
      ],
263 263
      "type": "object"
264 264
    },
265 265
    "size": {
266 266
      "properties": {
267 267
        "h": {
268 268
          "minimum": 1,
269 269
          "type": "integer"
270 270
        },
271 271
        "w": {
272 272
          "minimum": 1,
273 273
          "type": "integer"
274 274
        }
275 275
      },
276 276
      "required": [
277 277
        "w",
278 278
        "h"
279 279
      ],
280 280
      "type": "object"
281 281
    },
282 282
    "slice": {
283 283
      "properties": {
284 284
        "bounds": {
285 285
          "ref": "#rect",
286 286
          "type": "ref"
287 287
        },
288 288
        "data": {
289 289
          "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.",
290 290
          "items": {
291 291
            "ref": "#datum",
292 292
            "type": "ref"
293 293
          },
294 294
          "type": "array"
295 295
        },
296 296
        "name": {
297 297
          "maxLength": 64,
298 298
          "type": "string"
299 299
        }
300 300
      },
301 301
      "required": [
302 302
        "name",
303 303
        "bounds"
304 304
      ],
305 305
      "type": "object"
306 306
    },
307 307
    "sprite": {
308 308
      "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.",
309 309
      "properties": {
310 310
        "animations": {
311 311
          "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.",
312 312
          "items": {
313 313
            "ref": "#animation",
314 314
            "type": "ref"
315 315
          },
316 316
          "minLength": 1,
317 317
          "type": "array"
318 318
        },
319 319
        "atlas": {
320 320
          "accept": [
321 321
            "image/png"
322 322
          ],
323 323
          "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.",
324 324
          "maxSize": 1000000,
325 325
          "type": "blob"
326 326
        },
327 +
        "box": {
328 +
          "description": "Tight opaque bounds across EVERY FRAME OF EVERY ANIMATION, in source pixels, with the creature centred on its ground contact. The box a crowd view packs cells from.\n\n⚠ MEASURED FROM THE ATLAS AT BUILD TIME, NEVER AUTHORED. It lived in the client as a 76-entry table for exactly as long as the client also held the art; once the art moved to its own repo nothing in the client could re-derive it, and a table of measurements nobody can check is a table that rots silently — a clipped ear, a crowd quietly back to overlapping.\n\n⚠ NOT `frameSize`, WHICH IS THE CANVAS RATHER THAN THE ANIMAL. Widths here run from 13 to 69 against a frame that is typically 64×64, so sizing a cell from the frame leaves a piglin swimming in five times its own area.\n\nOPTIONAL, AND ABSENCE IS NOT AN ERROR: a reader without it falls back to a box large enough for anything drawn. Records published before this field existed simply do not have it.",
329 +
          "ref": "#size",
330 +
          "type": "ref"
331 +
        },
327 332
        "frameSize": {
328 333
          "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.",
329 334
          "ref": "#size",
330 335
          "type": "ref"
331 336
        },
337 +
        "portrait": {
338 +
          "description": "Tight opaque bounds of the FIRST FRAME OF THE `idle` TAG alone, same centring as `box`. The crop a single-pet portrait uses.\n\n⚠ A SECOND MEASUREMENT RATHER THAN A DERIVATION OF `box`, BECAUSE ONE FRAME IS NOT THE LOOP. The widest animal reaches 69 across its full run and 59 on its idle frame; a portrait cropped to 69 leaves every pet floating, and a crowd cell sized at 59 clips the frames the idle pose does not reach. Neither number can be computed from the other.\n\nOPTIONAL on the same terms as `box`.",
339 +
          "ref": "#size",
340 +
          "type": "ref"
341 +
        },
332 342
        "size": {
333 343
          "description": "The atlas's own pixel dimensions. Needed to place background-position offsets without waiting for the image to load.",
334 344
          "ref": "#size",
335 345
          "type": "ref"
336 346
        },
337 347
        "slices": {
338 348
          "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.",
339 349
          "items": {
340 350
            "ref": "#slice",
341 351
            "type": "ref"
342 352
          },
343 353
          "type": "array"
344 354
        }
345 355
      },
346 356
      "required": [
347 357
        "atlas",
348 358
        "size",
349 359
        "frameSize",
350 360
        "animations",
351 361
        "slices"
352 362
      ],
353 363
      "type": "object"
354 364
    }
355 365
  },
356 366
  "id": "pet.trezy.species",
357 367
  "lexicon": 1
358 368
}

Compare Other Versions

Lexicon Garden

@