pet.trezy.species
Schema Diff
+4 -0
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
"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
271
"jump"
268
272
]
269
273
},
270
274
"speed": {
271
275
"type": "integer",
272
276
"minimum": 0,
273
277
"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
278
},
275
279
"weight": {
276
280
"type": "integer",
277
281
"minimum": 0,
278
282
"description": "Relative likelihood against the species's other options. Zero is a legitimate way to keep a behaviour published but dormant."
279
283
},
280
284
"animation": {
281
285
"type": "string",
282
286
"maxLength": 64,
283
287
"description": "For `idle` and `move`: the animation to play throughout."
284
288
},
285
289
"maxDuration": {
286
290
"type": "integer",
287
291
"minimum": 0,
288
292
"description": "For `idle`: seconds, the longest."
289
293
},
290
294
"minDuration": {
291
295
"type": "integer",
292
296
"minimum": 0,
293
297
"description": "For `idle`: seconds, the shortest the pet will stand still."
294
298
},
295
299
"endAnimation": {
296
300
"type": "string",
297
301
"maxLength": 64,
298
302
"description": "For `slide` and `jump`: the recovery, played once."
299
303
},
300
304
"endMoveFrame": {
301
305
"type": "integer",
302
306
"minimum": 0,
303
307
"description": "For `jump`: the frame of the end animation at which it lands."
304
308
},
305
309
"loopAnimation": {
306
310
"type": "string",
307
311
"maxLength": 64,
308
312
"description": "For `slide` and `jump`: the sustained middle, repeated while travelling."
309
313
},
310
314
"startAnimation": {
311
315
"type": "string",
312
316
"maxLength": 64,
313
317
"description": "For `slide` and `jump`: the wind-up, played once."
314
318
},
315
319
"startMoveFrame": {
316
320
"type": "integer",
317
321
"minimum": 0,
318
322
"description": "For `jump`: the frame of the start animation at which the pet actually leaves the ground, so the launch matches the drawing."
319
323
}
320
324
},
321
325
"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
326
},
323
327
"animation": {
324
328
"type": "object",
325
329
"required": [
326
330
"name",
327
331
"frames"
328
332
],
329
333
"properties": {
330
334
"name": {
331
335
"type": "string",
332
336
"maxLength": 64,
333
337
"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
338
},
335
339
"frames": {
336
340
"type": "array",
337
341
"items": {
338
342
"ref": "#frame",
339
343
"type": "ref"
340
344
},
341
345
"minLength": 1
342
346
}
343
347
}
344
348
}
345
349
},
346
350
"$type": "com.atproto.lexicon.schema",
347
351
"lexicon": 1
348
352
}