Items
The Items resource is the catalog — one row per distinct item the game knows about. Player ownership lives in a separate table (item_instances) under /v1/users/@me/inventory.
Catalog is global and read-mostly: admins author rows, everyone reads. Reads need only an authenticated user; every write (POST, PATCH, DELETE, uploads) requires the items.write permission.
The Item object
- Name
id- Type
- string
- Description
Snowflake.
- Name
name- Type
- string
- Description
1–100 chars.
- Name
description- Type
- string
- Description
Up to 2000 chars. Defaults to
"".
- Name
type- Type
- ItemType
- Description
1DEFAULT,2EQUIPMENT,3DECOR,4FISH,5SCENERY.
- Name
rarity- Type
- Rarity
- Description
1COMMON →9UNIDENTIFIED. Drives roll bias on randomization + UI tint.
- Name
bind- Type
- BindType
- Description
1NONE,2BIND_ON_PICKUP,3BIND_ON_EQUIP.
- Name
icon- Type
- string
- Description
sha256[0:16] hash of the inventory thumbnail PNG. URL is
${CDN_BASE_URL}/icons/${icon}/${icon}.png. Defaults to"".
- Name
flags- Type
- integer
- Description
Bitfield.
1 << 0= DELUX. Leave room for new bits — never reuse.
- Name
properties- Type
- BaseItemProperties | null
- Description
Per-type bag of rollable property descriptors. See Properties.
- Name
prefab- Type
- any | null
- Description
Rendering hints. Non-randomizable items with a single sheet stash the spritesheet hash here as
{ spritesheet: "<hash>" }.
- Name
layers- Type
- ItemLayers | null
- Description
Opts the item into per-grant randomization. Null = the catalog defaults are reused as-is.
- Name
created_at- Type
- string
- Description
List
Filter by type, rarity, and a left-anchored name prefix q. Keyset-paginated.
- Name
type- Type
- integer
- Description
Optional ItemType filter.
- Name
rarity- Type
- integer
- Description
Optional Rarity filter.
- Name
q- Type
- string
- Description
Case-insensitive prefix on name.
- Name
before- Type
- string
- Description
Snowflake id for keyset pagination.
- Name
limit- Type
- integer
- Description
1–200. Default 50.
Request
curl "https://api.localuniverse.io/v1/items?type=4&limit=20" \
-H "authorization: <token>"
Response
[
{ "id": "3091...", "name": "Cosmic Bass", "type": 4, "rarity": 3, ... },
{ "id": "3092...", "name": "Sapphire Hat", "type": 2, "rarity": 4, ... }
]
Get, create, update, delete
| Method | Path | Permission | Notes |
|---|---|---|---|
GET | /v1/items/:itemId | none | Open to any authed user. |
POST | /v1/items | items.write | Body is a CreateItem shape — see below. |
PATCH | /v1/items/:itemId | items.write | PATCH semantics. properties, prefab, layers accept explicit null to clear. |
DELETE | /v1/items/:itemId | items.write | Cascades into every item_instance referencing the row. |
CreateItem body
- Name
name- Type
- string
- Description
1–100 chars.
- Name
description- Type
- ?string
- Description
- Name
type- Type
- ItemType
- Description
Required.
- Name
rarity- Type
- Rarity
- Description
Required.
- Name
bind- Type
- ?BindType
- Description
Defaults to NONE.
- Name
icon- Type
- ?string
- Description
Hash returned from
/v1/items/uploads/icon.
- Name
flags- Type
- ?integer
- Description
- Name
properties- Type
- ?any
- Description
- Name
prefab- Type
- ?any
- Description
For non-randomizable items with a spritesheet, set
{ spritesheet: "<hash>" }.
- Name
layers- Type
- ?ItemLayers
- Description
See Randomization.
Asset uploads
Three multipart endpoints. Each returns the content hash; the admin client stamps it onto the create-item payload.
Icon
PNG only. ≤ 8 MB. Server validates magic-bytes, hashes (sha256[0:16]), and stores at icons/<hash>/<hash>.png in CDN_BUCKET. Each asset lives in its own per-hash directory so PNG + sibling descriptors stay grouped.
Request
curl -X POST https://api.localuniverse.io/v1/items/uploads/icon \
-H "authorization: <token>" \
-F "file=@./icon.png"
Response
{ "hash": "a1b2c3d4e5f60123" }
Spritesheet
PNG only. ≤ 8 MB. Stored at textures/<hash>/<hash>.png.
The hash returned here is what you pass to /uploads/manifest to attach the manifest as a sibling in the same per-hash directory.
Request
curl -X POST https://api.localuniverse.io/v1/items/uploads/spritesheet \
-H "authorization: <token>" \
-F "file=@./spritesheet.png"
Manifest
JSON only. ≤ 1 MB. The server parses it as JSON for sanity but does not enforce shape — manifests are consumed by the client / game engine, not the API.
Stored at textures/<spritesheet_hash>/<spritesheet_hash>.spritesheet.json — same per-hash directory as the PNG, so the sprite-render service's copySiblingDescriptors walk picks it up when an instance composite is rendered.
Request
curl -X POST https://api.localuniverse.io/v1/items/uploads/manifest \
-H "authorization: <token>" \
-F "spritesheet_hash=a1b2c3d4e5f60123" \
-F "file=@./spritesheet.json"
Randomization
When layers is set on a catalog row, and the grant call opts in with randomize: true, the grant flow composites a unique spritesheet + icon per instance and stamps the resulting hashes onto the item_instance row.
items.layers shape
{
layers: number, // dye-zone count
hue_centers: number[], // original hues for roll bias
textures: string[], // texture names in the prefab that get tinted
icon: string // master icon name
}
The sprite renderer reads master _layer1.png…_layerN.png (plus optional _outlines.png) under textures/<master>/ in CDN_BUCKET, multiplies each by a rolled tint, composites, hashes the result, and writes it to textures/<hash>.png. Every sibling under the master directory matching <master>.<ext> (manifest, atlas, etc.) gets copied next to the hashed output. See packages/sprite-tools for the splitter that authors layer assets.
Properties
The catalog row's properties column is a Record<string, PropertyDescriptor> map. At grant time each descriptor rolls into a concrete value and the result is stamped onto item_instances.properties so it stays stable for the lifetime of the instance.
items.properties (catalog spec)
{
"equipment_type": { "value": 1, "resolve": "fixed" },
"durability": { "min": 50, "max": 100, "resolve": "range" },
"rarity_bonus": {
"min": 0, "max": 1, "mean": 0.3, "stddev": 0.15,
"precision": 2, "resolve": "curve"
},
"skin_variant": {
"values": ["red", "blue", "green"],
"weights": [3, 1, 1],
"resolve": "pool"
},
"blessed": { "value": true, "resolve": "fixed", "required": false, "probability": 0.05 }
}
item_instances.properties (rolled result)
{
"equipment_type": 1,
"durability": 87,
"rarity_bonus": 0.42,
"skin_variant": "red"
}
blessed was skipped in this roll — required: false plus probability: 0.05 means it lands on roughly 5% of grants and otherwise doesn't appear in the result at all.
Resolvers
- Name
fixed- Description
Literal pass-through.
{ value, resolve: 'fixed' }. Most catalog properties use this — even "random" stats are usually authored as afixedinteger first and switched to a roll later.
- Name
range- Description
Uniform pick
[min, max]. Integer-only when both bounds are integers andprecisionis omitted; otherwise float, optionally rounded toprecisiondecimal places.
- Name
pool- Description
Pick one of
valuesuniformly, or weighted byweights(same length). Strings, numbers, booleans all accepted.
- Name
curve- Description
Normal distribution centered on
mean(default: midpoint), withstddev(default: range / 6), clamped to[min, max]. Use for stat rolls where most instances should land near a centroid with rare outliers.
Every descriptor optionally carries required: false + probability to make the key itself probabilistic — useful for chance-of-drop modifiers like blessed, cursed, lucky, etc.
The whole roll lives in @localuniverse/common/property-resolver and is called once at grant time. Unknown resolve values throw — catalogs author against known strategies and a typo should fail loudly rather than silently dropping a property.
Inventory
Owned instances of catalog items live on the user resource:
| Method | Path | Notes |
|---|---|---|
GET | /v1/users/@me/inventory | The caller's instances. |
GET | /v1/users/@me/inventory/:instanceId | Single instance. |
DELETE | /v1/users/@me/inventory/:instanceId | Discard. |
POST | /v1/users/:userId/inventory | Admin grant. Body: { item_id, randomize? }. Requires items.write. |
Gameplay drops (fish caught, NPC reward) call itemInstanceService.grant() directly — they don't round-trip through HTTP.