pet.trezy.species

trezy.pet

Schema Diff

+19 -3

From

CID
bafyreigq7ha2axn...
Indexed At
2026-08-10 11:54 UTC
View this version

To

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

Compatibility Analysis

Breaking Changes Detected

1 breaking change, 8 non-breaking changes.

Breaking Changes (1)
  • ConstraintTightened ConstraintTightened { vertex_id: "pet.trezy.species#behavior.type", sort: "knownValues", old_value: "[\"idle\",\"move\",\"slide\",\"jump\"]", new_value: "[\"idle\",\"move\",\"slide\",\"jump\",\"fly\"]" }
Non-Breaking Changes (8)
  • AddedVertex AddedVertex { vertex_id: "pet.trezy.species#behavior.hoverAnimation" }
  • AddedVertex AddedVertex { vertex_id: "pet.trezy.species#sprite.box" }
  • AddedVertex AddedVertex { vertex_id: "pet.trezy.species#sprite.portrait" }
  • AddedEdge AddedEdge { src: "pet.trezy.species#behavior", tgt: "pet.trezy.species#behavior.hoverAnimation", kind: "prop", name: Some("hoverAnimation") }
  • 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#behavior.hoverAnimation" }
  • AddedVertex { vertex_id: "pet.trezy.species#sprite.box" }
  • AddedVertex { vertex_id: "pet.trezy.species#sprite.portrait" }

Constraint Changes

  • ConstraintTightened ConstraintTightened { vertex_id: "pet.trezy.species#behavior.type", sort: "knownValues", old_value: "[\"idle\",\"move\",\"slide\",\"jump\"]", new_value: "[\"idle\",\"move\",\"slide\",\"jump\",\"fly\"]" }

Additional Notes

  • Non-breaking: AddedEdge { src: "pet.trezy.species#behavior", tgt: "pet.trezy.species#behavior.hoverAnimation", kind: "prop", name: Some("hoverAnimation") }
  • 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 +
        "hoverAnimation": {
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 +
          "maxLength": 64,
47 +
          "type": "string"
48 +
        },
44 49
        "loopAnimation": {
45 50
          "description": "For `slide` and `jump`: the sustained middle, repeated while travelling.",
46 51
          "maxLength": 64,
47 52
          "type": "string"
48 53
        },
49 54
        "maxDuration": {
50 -
          "description": "For `idle`: seconds, the longest.",
55 +
          "description": "For `idle`: seconds, the longest. For `fly`: the longest it stays up.",
51 56
          "minimum": 0,
52 57
          "type": "integer"
53 58
        },
54 59
        "minDuration": {
55 -
          "description": "For `idle`: seconds, the shortest the pet will stand still.",
60 +
          "description": "For `idle`: seconds, the shortest the pet will stand still. For `fly`: seconds aloft before it looks for somewhere to land.",
56 61
          "minimum": 0,
57 62
          "type": "integer"
58 63
        },
59 64
        "speed": {
60 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.",
61 66
          "minimum": 0,
62 67
          "type": "integer"
63 68
        },
64 69
        "startAnimation": {
65 70
          "description": "For `slide` and `jump`: the wind-up, played once.",
66 71
          "maxLength": 64,
67 72
          "type": "string"
68 73
        },
69 74
        "startMoveFrame": {
70 75
          "description": "For `jump`: the frame of the start animation at which the pet actually leaves the ground, so the launch matches the drawing.",
71 76
          "minimum": 0,
72 77
          "type": "integer"
73 78
        },
74 79
        "type": {
75 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.",
76 81
          "knownValues": [
77 82
            "idle",
78 83
            "move",
79 84
            "slide",
80 -
            "jump"
85 +
            "jump",
86 +
            "fly"
81 87
          ],
82 88
          "maxLength": 32,
83 89
          "type": "string"
84 90
        },
85 91
        "weight": {
86 92
          "description": "Relative likelihood against the species's other options. Zero is a legitimate way to keep a behaviour published but dormant.",
87 93
          "minimum": 0,
88 94
          "type": "integer"
89 95
        }
90 96
      },
91 97
      "required": [
92 98
        "type",
93 99
        "weight"
94 100
      ],
95 101
      "type": "object"
96 102
    },
97 103
    "datum": {
98 104
      "properties": {
99 105
        "key": {
100 106
          "maxLength": 64,
101 107
          "type": "string"
102 108
        },
103 109
        "value": {
104 110
          "maxLength": 256,
105 111
          "type": "string"
106 112
        }
107 113
      },
108 114
      "required": [
109 115
        "key",
110 116
        "value"
111 117
      ],
112 118
      "type": "object"
113 119
    },
114 120
    "frame": {
115 121
      "description": "One frame: where it sits in the atlas, and how long it is held.",
116 122
      "properties": {
117 123
        "duration": {
118 124
          "description": "Milliseconds, as Aseprite exports it.",
119 125
          "minimum": 1,
120 126
          "type": "integer"
121 127
        },
122 128
        "h": {
123 129
          "minimum": 1,
124 130
          "type": "integer"
125 131
        },
126 132
        "w": {
127 133
          "minimum": 1,
128 134
          "type": "integer"
129 135
        },
130 136
        "x": {
131 137
          "minimum": 0,
132 138
          "type": "integer"
133 139
        },
134 140
        "y": {
135 141
          "minimum": 0,
136 142
          "type": "integer"
137 143
        }
138 144
      },
139 145
      "required": [
140 146
        "x",
141 147
        "y",
142 148
        "w",
143 149
        "h",
144 150
        "duration"
145 151
      ],
146 152
      "type": "object"
147 153
    },
148 154
    "main": {
149 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.",
150 156
      "key": "any",
151 157
      "record": {
152 158
        "properties": {
153 159
          "activity": {
154 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.)",
155 161
            "knownValues": [
156 162
              "diurnal",
157 163
              "nocturnal",
158 164
              "crepuscular",
159 165
              "matutinal",
160 166
              "vespertinal",
161 167
              "cathemeral"
162 168
            ],
163 169
            "maxLength": 64,
164 170
            "type": "string"
165 171
          },
166 172
          "behaviors": {
167 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.",
168 174
            "items": {
169 175
              "ref": "#behavior",
170 176
              "type": "ref"
171 177
            },
172 178
            "minLength": 1,
173 179
            "type": "array"
174 180
          },
175 181
          "displayName": {
176 182
            "maxLength": 64,
177 183
            "type": "string"
178 184
          },
179 185
          "groups": {
180 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`.",
181 187
            "items": {
182 188
              "maxLength": 64,
183 189
              "type": "string"
184 190
            },
185 191
            "type": "array"
186 192
          },
187 193
          "id": {
188 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.",
189 195
            "maxLength": 64,
190 196
            "type": "string"
191 197
          },
192 198
          "price": {
193 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.",
194 200
            "minimum": 0,
195 201
            "type": "integer"
196 202
          },
197 203
          "purchasable": {
198 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.",
199 205
            "type": "boolean"
200 206
          },
201 207
          "sprite": {
202 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.",
203 209
            "ref": "#sprite",
204 210
            "type": "ref"
205 211
          },
206 212
          "starter": {
207 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.",
208 214
            "type": "boolean"
209 215
          },
210 216
          "tier": {
211 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.",
212 218
            "knownValues": [
213 219
              "common",
214 220
              "uncommon",
215 221
              "rare",
216 222
              "epic",
217 223
              "legendary"
218 224
            ],
219 225
            "maxLength": 64,
220 226
            "type": "string"
221 227
          }
222 228
        },
223 229
        "required": [
224 230
          "id",
225 231
          "displayName",
226 232
          "tier",
227 233
          "sprite",
228 234
          "behaviors"
229 235
        ],
230 236
        "type": "object"
231 237
      },
232 238
      "type": "record"
233 239
    },
234 240
    "rect": {
235 241
      "properties": {
236 242
        "h": {
237 243
          "minimum": 0,
238 244
          "type": "integer"
239 245
        },
240 246
        "w": {
241 247
          "minimum": 0,
242 248
          "type": "integer"
243 249
        },
244 250
        "x": {
245 251
          "type": "integer"
246 252
        },
247 253
        "y": {
248 254
          "type": "integer"
249 255
        }
250 256
      },
251 257
      "required": [
252 258
        "x",
253 259
        "y",
254 260
        "w",
255 261
        "h"
256 262
      ],
257 263
      "type": "object"
258 264
    },
259 265
    "size": {
260 266
      "properties": {
261 267
        "h": {
262 268
          "minimum": 1,
263 269
          "type": "integer"
264 270
        },
265 271
        "w": {
266 272
          "minimum": 1,
267 273
          "type": "integer"
268 274
        }
269 275
      },
270 276
      "required": [
271 277
        "w",
272 278
        "h"
273 279
      ],
274 280
      "type": "object"
275 281
    },
276 282
    "slice": {
277 283
      "properties": {
278 284
        "bounds": {
279 285
          "ref": "#rect",
280 286
          "type": "ref"
281 287
        },
282 288
        "data": {
283 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.",
284 290
          "items": {
285 291
            "ref": "#datum",
286 292
            "type": "ref"
287 293
          },
288 294
          "type": "array"
289 295
        },
290 296
        "name": {
291 297
          "maxLength": 64,
292 298
          "type": "string"
293 299
        }
294 300
      },
295 301
      "required": [
296 302
        "name",
297 303
        "bounds"
298 304
      ],
299 305
      "type": "object"
300 306
    },
301 307
    "sprite": {
302 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.",
303 309
      "properties": {
304 310
        "animations": {
305 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.",
306 312
          "items": {
307 313
            "ref": "#animation",
308 314
            "type": "ref"
309 315
          },
310 316
          "minLength": 1,
311 317
          "type": "array"
312 318
        },
313 319
        "atlas": {
314 320
          "accept": [
315 321
            "image/png"
316 322
          ],
317 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.",
318 324
          "maxSize": 1000000,
319 325
          "type": "blob"
320 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 +
        },
321 332
        "frameSize": {
322 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.",
323 334
          "ref": "#size",
324 335
          "type": "ref"
325 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 +
        },
326 342
        "size": {
327 343
          "description": "The atlas's own pixel dimensions. Needed to place background-position offsets without waiting for the image to load.",
328 344
          "ref": "#size",
329 345
          "type": "ref"
330 346
        },
331 347
        "slices": {
332 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.",
333 349
          "items": {
334 350
            "ref": "#slice",
335 351
            "type": "ref"
336 352
          },
337 353
          "type": "array"
338 354
        }
339 355
      },
340 356
      "required": [
341 357
        "atlas",
342 358
        "size",
343 359
        "frameSize",
344 360
        "animations",
345 361
        "slices"
346 362
      ],
347 363
      "type": "object"
348 364
    }
349 365
  },
350 366
  "id": "pet.trezy.species",
351 367
  "lexicon": 1
352 368
}

Compare Other Versions

Lexicon Garden

@