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