pet.trezy.species

trezy.pet

Schema Diff

+13 -3

From

CID
bafyreigtodk3l27...
Indexed At
2026-08-09 01:42 UTC
View this version

To

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

Compatibility Analysis

Breaking Changes Detected

1 breaking change, 4 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 (4)
  • AddedVertex AddedVertex { vertex_id: "pet.trezy.species#behavior.hoverAnimation" }
  • AddedVertex AddedVertex { vertex_id: "pet.trezy.species:body.starter" }
  • AddedEdge AddedEdge { src: "pet.trezy.species#behavior", tgt: "pet.trezy.species#behavior.hoverAnimation", kind: "prop", name: Some("hoverAnimation") }
  • AddedEdge AddedEdge { src: "pet.trezy.species:body", tgt: "pet.trezy.species:body.starter", kind: "prop", name: Some("starter") }

Migration Guidance

Added Elements

  • AddedVertex { vertex_id: "pet.trezy.species#behavior.hoverAnimation" }
  • AddedVertex { vertex_id: "pet.trezy.species:body.starter" }

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

Compare Other Versions

Lexicon Garden

@