pet.trezy.species

trezy.pet

Schema Diff

+4 -0

From

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

To

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

Compatibility Analysis

Backward Compatible

Backward compatible. 2 non-breaking changes.

Non-Breaking Changes (2)
  • AddedVertex AddedVertex { vertex_id: "pet.trezy.species:body.starter" }
  • 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:body.starter" }

Additional Notes

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

Compare Other Versions

Lexicon Garden

@