pet.trezy.species
Schema Diff
+10 -0
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
}