Trades
phantom.aj.tradesOther players
Trading: trade lists, offers, the trade window.
Scripts and plugins
A script has all of it, as phantom.aj.trades, and so does a plugin, from ctx.phantom(): the SDK runs in the plugin's own frame. Each call is marked with what it needs: game:read for what the area knows and hears, which "game": "read" in the manifest grants, and game:act for doing things in it as the player, which needs "game": "act" (see Permissions).
Overview
| Member | What it does | Plugins |
|---|---|---|
accept() | Accepts the open offer, as the offer window's yes button does (onConfirmAcceptTrade), and resolves once the server asks both sides to confirm. | game:act |
add() | Puts items up for trade, as closing the avatar editor with items marked does (TradeManager.changeTradeList), and resolves with the trade list afterwards. | game:act |
cancel() | Withdraws the player's own request, as closing the waiting window does: cancels it (sendTradeItemCancel), stops being busy and closes the windows. | game:act |
close() | Closes every trade window and ends the trade in the client, as the trade done window's OK does: the one step the client leaves to the player after a trade that went through. | game:act |
confirm() | Answers the server's ask to confirm (sendConfirmTrade), as the review window's buttons do. | game:act |
decline() | Declines the open offer, as the offer window's no button does (onDeclineTrade): refuses it, stops being busy and closes the windows. | game:act |
findOnList() | Finds an item on a trade list by what it is (kind, def id, a colour for clothing), giving the place request asks for it by, or null. | game:read |
isTrading() | Whether the client counts itself in a trade (TradeManager.isCurrentlyTrading). | game:read |
list() | Someone's trade list, asked of the server as the buddy card's trade tab does (sendTradeListRequest), resolving with the server's answer. | game:read |
listed() | Everything the player has up for trade, as the client holds it (UserInfoCache._myTradeList), each by kind (TRADE_ITEM_TYPE) and inventory place. | game:read |
myList() | The player's own trade list, as the server shows it to everyone else. | game:act |
offer() | The offer made to the player that is still open, or null. | game:read |
remove() | Takes items off the trade list (see add), resolving with the list afterwards. | game:act |
request() | Asks someone for one of their items, offering some of the player's own, as the trade window's send button does (setupAndSendTradeRequest): marks the player busy and trading, sends the request, and shows the window a request waits in. | game:act |
state() | The trade in progress, both sides, in one look: how far it has got (from the client's own windows), who with, the open offer, the player's trade list and the request being put together. | game:read |
turnAwayOffers() | Turns every offer away (sendTradeBusyRequest(true)), as an open trade window does, until the handle it resolves with is called. | game:act |
waitFor() | Waits for the next step of a trade: 'offer' resolves with the offer made, 'confirm' when the server asks to confirm, 'completed' with what changed hands. | game:read |
onChange() | Hears every step of every trade: offered with the offer, confirm when the server asks to confirm, completed with what changed hands, ended with why. | game:read |
onList() | Hears someone's trade list arrive, whoever asked for it (trade.list). | game:read |
onListChange() | Hears the player's trade list change, as the game sends the change (trade.list-change): add, remove, or the avatar editor closing with items marked. | game:read |
onOffer() | Hears someone offer the player a trade. | game:read |
trades.window: The game's own trade request window, where the player picks what to offer.
| Member | What it does | Plugins |
|---|---|---|
add() | Adds one of the player's items to the offer in the window, as its item picker does (TradeManager.addTradeItem), and resolves with the offer afterwards. | game:act |
draft() | What the player has picked to offer in the window and not sent yet (TradeManager.initiationTradeList), in the order picked. | game:read |
isOpen() | Whether the trade request window is on screen. | game:read |
open() | Opens the window against one of someone's items, as their buddy card's trade button does, and resolves once it is on screen. | game:act |
remove() | Takes an item back out of the offer, as pressing the remove button on its box does, and resolves with the offer afterwards. | game:act |
send() | Presses send, as the player would (TradeManager.onDoneBtn). | game:act |
onChange() | Hears the window come up (true) and go (false), however it was opened. | game:read |
trades.itemPicker: The item picker the trade, gift and den shop windows open to choose an item.
| Member | What it does | Plugins |
|---|---|---|
clearGift() | Takes back the gift chosen for a Jam-a-Gram (removeGift). | game:act |
clearSearch() | Clears the search (clearCurrentSearchText). | game:act |
current() | The picker as it stands, or null when none is open. | game:read |
pressSlot() | Presses one of the grid's slots, as the player does, which chooses that item. | game:act |
search() | Narrows the list to items whose names hold text, as typing in its search box does. | game:act |
setSort() | Sorts the list shown, by the picker's own sort numbers (1 to 7 for clothes, 1 to 6 for den items), and redraws it. | game:act |
showTab() | Shows one of its lists, as its tab does. | game:act |
slots() | The slots of the grid it shows. | game:read |
onChange() | Hears the picker ready, change, an item picked, a gift chosen or taken back, and closing. | game:read |
Methods
accept
accept(options?: ActOptions) => Promise<void>Accepts the open offer, as the offer window's yes button does (onConfirmAcceptTrade), and resolves once the server asks both sides to confirm. Throws REFUSED with no offer open, when the player has no room for what is offered, or with the server's reason when the trade ends.
Parameters
| Name | Type | |
|---|---|---|
options optional | ActOptions |
Returns
Promise<void>
Types
add
add(items: TradeItemRef | readonly TradeItemRef[], options?: ActOptions) => Promise<TradeItemRef[]>Puts items up for trade, as closing the avatar editor with items marked does (TradeManager.changeTradeList), and resolves with the trade list afterwards. Items already up are left as they are. Throws REFUSED when the client's list does not take them.
Parameters
| Name | Type | |
|---|---|---|
items | TradeItemRef | readonly TradeItemRef[] | |
options optional | ActOptions |
Returns
Promise<TradeItemRef[]>
Types
Example
cancel
cancel(options?: ActOptions) => Promise<void>Withdraws the player's own request, as closing the waiting window does: cancels it (sendTradeItemCancel), stops being busy and closes the windows.
Parameters
| Name | Type | |
|---|---|---|
options optional | ActOptions |
Returns
Promise<void>
Types
close
close(options?: ActOptions) => Promise<void>Closes every trade window and ends the trade in the client, as the trade done window's OK does: the one step the client leaves to the player after a trade that went through. Sends no refusal; safe with nothing open.
Parameters
| Name | Type | |
|---|---|---|
options optional | ActOptions |
Returns
Promise<void>
Types
confirm
confirm(options?: ActOptions & { accept?: boolean | undefined; }) => Promise<void>Answers the server's ask to confirm (sendConfirmTrade), as the review window's buttons do. confirm() goes through with the trade and resolves once the game has sent that; the trade completes when both sides have, which waitFor('completed') waits for. confirm({ accept: false }) backs out, and resolves once the client has left the trade.
Parameters
| Name | Type | |
|---|---|---|
options optional | ActOptions & { accept?: boolean | undefined; } |
Returns
Promise<void>
Types
decline
decline(options?: ActOptions) => Promise<void>Declines the open offer, as the offer window's no button does (onDeclineTrade): refuses it, stops being busy and closes the windows. Resolves once the client has left the trade.
Parameters
| Name | Type | |
|---|---|---|
options optional | ActOptions |
Returns
Promise<void>
Types
findOnList
findOnList(listing: TradeListing, wanted: TradeItemLike) => TradeItemRef | nullFinds an item on a trade list by what it is (kind, def id, a colour for clothing), giving the place request asks for it by, or null. Sends nothing.
Parameters
| Name | Type | |
|---|---|---|
listing | TradeListing | |
wanted | TradeItemLike |
Returns
TradeItemRef \| null
Types
TradeListing · TradeItemLike · TradeItemRef
Example
const wanted = phantom.aj.trades.findOnList(await phantom.aj.trades.list('friend'), {
kind: 'clothing', defId: 1140, color: 42,
});isTrading
isTrading() => Promise<boolean>Whether the client counts itself in a trade (TradeManager.isCurrentlyTrading).
Returns
Promise<boolean>
list
list(userName: string, options?: ActOptions) => Promise<TradeListing>Someone's trade list, asked of the server as the buddy card's trade tab does (sendTradeListRequest), resolving with the server's answer.
Parameters
| Name | Type | |
|---|---|---|
userName | string | |
options optional | ActOptions |
Returns
Promise<TradeListing>
Types
listed
listed() => Promise<TradeItemRef[]>Everything the player has up for trade, as the client holds it (UserInfoCache._myTradeList), each by kind (TRADE_ITEM_TYPE) and inventory place. Sends nothing; myList asks the server.
Returns
Promise<TradeItemRef[]>
Types
myList
myList(options?: ActOptions) => Promise<TradeListing>The player's own trade list, as the server shows it to everyone else.
Parameters
| Name | Type | |
|---|---|---|
options optional | ActOptions |
Returns
Promise<TradeListing>
Types
offer
offer() => Promise<TradeOffer | null>The offer made to the player that is still open, or null. Kept from the server's offer (see the class), and given only while the client is in a trade with whoever made it.
Returns
Promise<TradeOffer \| null>
Types
TradeOffer · TradeItems · TradeClothing · TradeDenItem · TradePet
remove
remove(items: TradeItemRef | readonly TradeItemRef[], options?: ActOptions) => Promise<TradeItemRef[]>Takes items off the trade list (see add), resolving with the list afterwards.
Parameters
| Name | Type | |
|---|---|---|
items | TradeItemRef | readonly TradeItemRef[] | |
options optional | ActOptions |
Returns
Promise<TradeItemRef[]>
Types
request
request(userName: string, wanted: TradeItemRef, offer: TradeOfferItems, options?: ActOptions) => Promise<TradeRequest>Asks someone for one of their items, offering some of the player's own, as the trade window's send button does (setupAndSendTradeRequest): marks the player busy and trading, sends the request, and shows the window a request waits in. Resolves once the game has sent it; the other player answers in their own time, which waitFor('confirm') waits for.
The wanted item is named as their trade list gives it (list, findOnList); the offer by each item's place in the player's inventory. Throws REFUSED when the player is already trading, or the offer is empty or over twenty items.
Parameters
| Name | Type | |
|---|---|---|
userName | string | |
wanted | TradeItemRef | |
offer | TradeOfferItems | |
options optional | ActOptions |
Returns
Promise<TradeRequest>
Types
state
state() => Promise<TradeState>The trade in progress, both sides, in one look: how far it has got (from the client's own windows), who with, the open offer, the player's trade list and the request being put together.
Returns
Promise<TradeState>
Types
turnAwayOffers
turnAwayOffers(options?: ActOptions) => Promise<() => Promise<void>>Turns every offer away (sendTradeBusyRequest(true)), as an open trade window does, until the handle it resolves with is called. Other players are told the player is already in a trade.
Parameters
| Name | Type | |
|---|---|---|
options optional | ActOptions |
Returns
Promise<() => Promise<void>>
Types
Example
waitFor
waitFor<Step extends keyof TradeStepPayload>(step: Step, options?: ActOptions) => Promise<TradeStepPayload[Step]>Waits for the next step of a trade: 'offer' resolves with the offer made, 'confirm' when the server asks to confirm, 'completed' with what changed hands. Throws REFUSED with the server's reason when the trade ends first, and TIMEOUT after a minute by default.
Parameters
| Name | Type | |
|---|---|---|
step | Step | |
options optional | ActOptions |
Returns
Promise<TradeStepPayload[Step]>
Types
Example
const offer = await phantom.aj.trades.waitFor('offer', { timeoutMs: 120_000 });Events
onChange
onChange(listener: (change: TradeChange) => void) => () => voidHears every step of every trade: offered with the offer, confirm when the server asks to confirm, completed with what changed hands, ended with why.
Parameters
| Name | Type | |
|---|---|---|
listener | (change: TradeChange) => void |
Returns
() => void. Call it to stop listening.
Types
Example
phantom.aj.trades.onChange((change) => {
if (change.step === 'ended') console.log(change.end.message);
});onList
onList(listener: (listing: TradeListing) => void) => () => voidHears someone's trade list arrive, whoever asked for it (trade.list).
Parameters
| Name | Type | |
|---|---|---|
listener | (listing: TradeListing) => void |
Returns
() => void. Call it to stop listening.
Types
onListChange
onListChange(listener: (edit: TradeListEdit) => void) => () => voidHears the player's trade list change, as the game sends the change (trade.list-change): add, remove, or the avatar editor closing with items marked.
Parameters
| Name | Type | |
|---|---|---|
listener | (edit: TradeListEdit) => void |
Returns
() => void. Call it to stop listening.
Types
onOffer
onOffer(listener: (offer: TradeOffer) => void) => () => voidHears someone offer the player a trade. Answer with accept or decline.
Parameters
| Name | Type | |
|---|---|---|
listener | (offer: TradeOffer) => void |
Returns
() => void. Call it to stop listening.
Types
trades.window
The game's own trade request window, where the player picks what to offer.
add
add(item: TradeItemRef, options?: ActOptions) => Promise<TradeDraftItem[]>Adds one of the player's items to the offer in the window, as its item picker does (TradeManager.addTradeItem), and resolves with the offer afterwards. Throws REFUSED with the window closed, twenty items already offered, or an item that is not the player's, not approved yet, or listed in their den shop.
Parameters
| Name | Type | |
|---|---|---|
item | TradeItemRef | |
options optional | ActOptions |
Returns
Promise<TradeDraftItem[]>
Types
draft
draft() => Promise<TradeDraftItem[]>What the player has picked to offer in the window and not sent yet (TradeManager.initiationTradeList), in the order picked.
Returns
Promise<TradeDraftItem[]>
Types
isOpen
isOpen() => Promise<boolean>Whether the trade request window is on screen.
Returns
Promise<boolean>
open
open(userName: string, wanted: TradeItemRef, options?: ActOptions) => Promise<void>Opens the window against one of someone's items, as their buddy card's trade button does, and resolves once it is on screen. The client finds the wanted item only on the buddy card's copy of their trade list, so this opens the card, asks for their list, checks the item is on it, and then opens the window (displayRequestTrade). Throws REFUSED when the player is already trading or the item is not up for trade, and TIMEOUT when the card or the window never comes.
Parameters
| Name | Type | |
|---|---|---|
userName | string | |
wanted | TradeItemRef | |
options optional | ActOptions |
Returns
Promise<void>
Types
remove
remove(item: TradeItemRef, options?: ActOptions) => Promise<TradeDraftItem[]>Takes an item back out of the offer, as pressing the remove button on its box does, and resolves with the offer afterwards. Throws REFUSED with the window closed or the item not in the offer.
Parameters
| Name | Type | |
|---|---|---|
item | TradeItemRef | |
options optional | ActOptions |
Returns
Promise<TradeDraftItem[]>
Types
send
send(options?: ActOptions) => Promise<TradeWindowSend>Presses send, as the player would (TradeManager.onDoneBtn). Resolves 'sent' once the game has sent the request, or 'asking' when the offer holds a rare or diamond item and the client asks "are you sure" first (TradeConfirmPopup), which the player answers. Throws REFUSED with the window closed or nothing offered.
Parameters
| Name | Type | |
|---|---|---|
options optional | ActOptions |
Returns
Promise<TradeWindowSend>
Types
onChange
onChange(listener: (open: boolean) => void) => () => voidHears the window come up (true) and go (false), however it was opened. The reading taken as the watch starts is not told.
Parameters
| Name | Type | |
|---|---|---|
listener | (open: boolean) => void |
Returns
() => void. Call it to stop listening.
trades.itemPicker
The item picker the trade, gift and den shop windows open to choose an item.
clearGift
clearGift(options?: ActOptions) => Promise<ItemPickerView>Takes back the gift chosen for a Jam-a-Gram (removeGift).
Parameters
| Name | Type | |
|---|---|---|
options optional | ActOptions |
Returns
Promise<ItemPickerView>
Types
clearSearch
clearSearch(options?: ActOptions) => Promise<ItemPickerView>Clears the search (clearCurrentSearchText).
Parameters
| Name | Type | |
|---|---|---|
options optional | ActOptions |
Returns
Promise<ItemPickerView>
Types
current
current() => Promise<ItemPickerView | null>The picker as it stands, or null when none is open.
Returns
Promise<ItemPickerView \| null>
Types
pressSlot
pressSlot(index: number, options?: ActOptions) => Promise<ItemPickerView>Presses one of the grid's slots, as the player does, which chooses that item.
Parameters
| Name | Type | |
|---|---|---|
index | number | |
options optional | ActOptions |
Returns
Promise<ItemPickerView>
Types
search
search(text: string, options?: ActOptions) => Promise<ItemPickerView>Narrows the list to items whose names hold text, as typing in its search box does.
Parameters
| Name | Type | |
|---|---|---|
text | string | |
options optional | ActOptions |
Returns
Promise<ItemPickerView>
Types
setSort
setSort(sort: number, options?: ActOptions) => Promise<ItemPickerView>Sorts the list shown, by the picker's own sort numbers (1 to 7 for clothes, 1 to 6 for den items), and redraws it. The picker remembers it for its purpose.
Parameters
| Name | Type | |
|---|---|---|
sort | number | |
options optional | ActOptions |
Returns
Promise<ItemPickerView>
Types
showTab
showTab(tab: ItemPickerTab, options?: ActOptions) => Promise<ItemPickerView>Shows one of its lists, as its tab does.
Parameters
| Name | Type | |
|---|---|---|
tab | ItemPickerTab | |
options optional | ActOptions |
Returns
Promise<ItemPickerView>
Types
slots
slots() => Promise<ItemPickerSlot[]>The slots of the grid it shows.
Returns
Promise<ItemPickerSlot[]>
Types
onChange
onChange(listener: (change: ItemPickerChange) => void) => () => voidHears the picker ready, change, an item picked, a gift chosen or taken back, and closing.
Parameters
| Name | Type | |
|---|---|---|
listener | (change: ItemPickerChange) => void |
Returns
() => void. Call it to stop listening.
Types
Types
The types the calls above take and give, with every field. Import any of them in a plugin from @phantomclient/plugin/sdk, or use them as they are in a script.
TradeOffer
An offer someone has made the player.
| Field | Type | |
|---|---|---|
userName | string | Who made it. |
nameShown | boolean | Whether the game may show their name (the server's moderation flag). |
wanted | { kind: number; itemType: number | null; invIdx: number; } | The player's item they want. kind is the offer's own number (0 to 2 clothing, 3 to 5 den items by world, 6 a pet); itemType the same in TRADE_ITEM_TYPE, null for a number no offer uses. |
offered | TradeItems | What they give for it. |
TradeItems
What is offered in a trade: clothing, den items and pets.
| Field | Type | |
|---|---|---|
clothing | TradeClothing[] | |
den | TradeDenItem[] | |
pets | TradePet[] |
TradeClothing
A piece of clothing: its definition, its colour and, when the owner is known, its place.
| Field | Type | |
|---|---|---|
defId | number | The clothing's definition id. |
color | number | Its colour, as one packed number. |
invIdx | number | null | Its place in the owner's inventory, or null where the message gives none (an offer). |
TradeDenItem
A den item. approved and the unique image describe a custom item, such as a masterpiece.
| Field | Type | |
|---|---|---|
defId | number | The den item's definition id. |
invIdx | number | null | Its place in the owner's inventory, or null where the message gives none (an offer). |
version | number | The item's version (DenItem.init). |
approved | boolean | A custom item's image is approved. Sent as the text true or false. |
uniqueImageId | string | A custom item's image id; empty for an ordinary item. |
creatorDbId | number | The database id of the player who made a custom item. |
creatorUuid | string | The UUID of the player who made a custom item. |
TradePet
A pet (pet.PetItem): its three packed looks, and what it likes.
| Field | Type | |
|---|---|---|
defId | number | The pet's definition id. |
invIdx | number | Its place in the owner's pet inventory. |
createdAt | number | When it was adopted, as the server sends it. |
bits | [number, number, number] | Its look, packed into three numbers (pet.PetItem). |
name | string | The name the owner gave it. |
traitDefId | number | The definition id of its trait. |
toyDefId | number | The definition id of its favourite toy. |
foodDefId | number | The definition id of its favourite food. |
TradeItemRef
An item by its kind and its place in its owner's inventory, as trading names one.
| Field | Type | |
|---|---|---|
itemType | number | |
invIdx | number |
TradeState
The trade in progress, both sides, as state() reads it.
| Field | Type | |
|---|---|---|
stage | TradeStage | |
trading | boolean | Whether the client counts itself in a trade (TradeManager.isCurrentlyTrading). |
userName | string | null | The other player: who made the offer, or who the request is for; null with no trade. |
offer | TradeOffer | null | The offer being answered, while there is one. |
inventoryFull | boolean | Taking what is offered would overfill the player's inventory, so accepting it fails. |
myList | TradeItemRef[] | Everything the player has up for trade. |
draft | TradeDraftItem[] | What the player has picked in the trade window and not sent. |
TradeStage
How far a trade has got, as the client's own windows show it.
none: no trade. choosing: the trade request window is up, for the player to pick an offer. waiting: the player's request is out. offered: an offer is waiting on accept or decline. confirming: both sides agreed and the client asks for confirm. done: the trade done window is up, close takes it down. trading: the client counts itself in a trade with no window up.
type TradeStage = 'none' | 'choosing' | 'waiting' | 'offered' | 'confirming' | 'done' | 'trading'TradeDraftItem
One item in the trade request being put together in the trade window.
type TradeDraftItem = TradeItemRef & { defId: number }TradeListing
Someone's trade list, as the server sends it: what they have up for trade.
| Field | Type | |
|---|---|---|
userName | string | |
clothing | { defId: number; invIdx: number; color: number; }[] | |
den | (TradeDenItem & { invIdx: number; })[] | |
pets | TradePet[] |
TradeItemLike
What an item is rather than where it sits: its kind, its definition and, for clothing, its colour. findOnList turns one into the place request asks for.
| Field | Type | |
|---|---|---|
kind | "clothing" | "den" | "pet" | |
defId | number | |
color optional | number | null | undefined | Clothing's colour: two of one item in two colours are two things. Left out, any will do. |
ActOptions
How long something that acts may take, and what may end it first.
| Field | Type | |
|---|---|---|
timeoutMs optional | number | undefined | 10 seconds by default. |
signal optional | AbortSignal | undefined |
TradeOfferItems
Counts an offer by kind, as a request sends it.
| Field | Type | |
|---|---|---|
clothing optional | readonly number[] | undefined | Clothing, by invIdx. |
den optional | readonly number[] | undefined | Den items that are not audio items. |
audio optional | readonly number[] | undefined | Audio den items, which a request counts apart. |
pets optional | readonly number[] | undefined |
TradeRequest
A request the player has out with someone.
| Field | Type | |
|---|---|---|
userName | string | |
wanted | TradeItemRef | |
offered | Required<{ clothing?: number[] | undefined; den?: number[] | undefined; audio?: number[] | undefined; pets?: number[] | undefined; }> |
TradeChange
Each step of a trade, as one event.
type TradeChange = | { step: 'offered'; offer: TradeOffer }
/** Both sides have agreed, and the server asks the player to confirm. */
| { step: 'confirm' }
| { step: 'completed'; completion: TradeCompletion }
| { step: 'ended'; end: TradeEnd }TradeListEdit
A change the player made to their trade list, as the game sent it (trade.list-change).
| Field | Type | |
|---|---|---|
added | InventoryRef[] | |
removed | InventoryRef[] |
InventoryRef
An item by its type and place: 0 clothing, 1 den item, 3 pet (TradeItem).
| Field | Type | |
|---|---|---|
itemType | number | The item's type: 0 clothing, 1 den item, 3 pet. |
invIdx | number | Its place in the player's inventory. |
TradeWindowSend
What pressing send did: the request went, or the client asks "are you sure" over a rare item.
type TradeWindowSend = 'sent' | 'asking'ItemPickerView
The item picker (gui.DenAndClothesItemSelect), as it stands.
| Field | Type | |
|---|---|---|
purpose | "gift" | "trade" | "denShop" | "tradeStart" | What it is choosing for: a trade, a Jam-a-Gram gift, the den shop, or starting a trade. |
tab | ItemPickerTab | |
hasPets | boolean | |
search | string | |
searching | boolean | The search box is open. |
giftInvIdx | number | null | The gift chosen, by inventory slot, for a Jam-a-Gram. |
counts | { clothes: number; denItems: number; pets: number; } | How many of each list it holds. |
ItemPickerTab
Which list the item picker shows.
type ItemPickerTab = 'clothes' | 'den' | 'pets'ItemPickerSlot
One slot of the picker's grid.
| Field | Type | |
|---|---|---|
index | number | |
name | string | null | |
defId | number | null | |
invIdx | number | null | |
shown | boolean | |
drawn | boolean |
ItemPickerChange
Something happening on the picker.
type ItemPickerChange = | {
kind: 'ready' | 'changed';
picker: Pick<ItemPickerView, 'purpose' | 'tab' | 'hasPets' | 'search'>;
}
| {
kind: 'picked';
index: number;
name: string | null;
defId: number | null;
invIdx: number | null;
}
| { kind: 'gift'; chosen: boolean }
| { kind: 'closed' }