Skip to content

Quick start

Every example on this page is a piece of one panel, a general store.

A component is a function whose name starts with a capital letter and whose return type is Component:

src/shop.ts2
const ShopInfo = (): Component => (
<SunkenBox>
<text x="center" y={12} w={{ fill: 16 }} h={48} font={Fonts.PLAIN}
text="This shop restocks every few minutes. Selling to it lowers what it pays."
align="center" shadow color={Colors.DIM} lineHeight={16} />
</SunkenBox>
);

Lowercase tags such as <text> are the seven built-in elements. Capitalised tags such as <SunkenBox> are components, and you nest them the same way:

src/shop.ts2
export const Shop = (): Component => (
<Window title="General Store">
<layer x={10} y={42} w={{ fill: 20 }} h={{ fill: 52 }}>
<ShopTabs />
<ShopContents />
<CoinPurse />
</layer>
</Window>
);

Window and SunkenBox are not part of ts2. They are ordinary components in the project’s src/chrome.ts2.

A component returns one element. Wrap siblings in a <layer>, or in <>…</> if you want no container; return null to draw nothing. Close every tag, and put expressions in braces:

src/shop.ts2
<text x="right" y="center" w={60} h={16} text={`${price} gp`} font={Fonts.PLAIN}
align="right" valign="middle" shadow color={Colors.AMBER} />

Every element takes x, y, w and h, as pixels or keywords:

src/shop.ts2
<layer x={10} y={42} w={{ fill: 20 }} h={{ fill: 52 }}>
x="left" | "center" | "right"anchored to an edge of the parent, or centred
y="top" | "center" | "bottom"the same, vertically
w="fill" / h="fill"as large as the parent
w="50%"half the parent
w={{ fill: 8 }}the parent, less 8 pixels
x={{ center: -20 }}centred, then shifted 20 left

Layout has the full set.

A component receives props through its one parameter, destructured and typed:

src/shop.ts2
const StockRow = ({ item, name, price, stock }: {
item: Item; name: String; price: Int; stock: Int;
}): Component => (
<layer x={6} w={{ fill: 12 }} h={ROW_H}>
…
<sprite x={0} y="center" w={36} h={32} item={item} itemCount={-1} />

You pass them like attributes:

src/shop.ts2
<StockRow item={row.item} name={row.name} price={row.price} stock={row.stock} y={4 + i * ROW_H} />

StockRow does not declare y, so it lands on the row’s root <layer>.

Use && to draw something or nothing:

src/shop.ts2
{stock > 0 && (
<text x={44} y={19} w={116} h={12} text={`${stock} in stock`} font={Fonts.SMALL}
valign="middle" shadow color={Colors.DIM} />
)}

Use ? : to choose between two things. Either arm may be null:

src/shop.ts2
{stock === 0
? (
<text x="right" y="center" w={60} h={16} text="Sold out" font={Fonts.PLAIN}
align="right" valign="middle" shadow color={Colors.RED} />
)
: (
<text x="right" y="center" w={60} h={16} text={`${price} gp`} font={Fonts.PLAIN}
align="right" valign="middle" shadow color={Colors.AMBER} />
)}

Write stock === 0, not !stock.

A constant array can hold records, and each field keeps its type:

src/shop.ts2
const STOCK = [
{ item: Items.BRONZE_DAGGER, name: 'Bronze dagger', price: 12, stock: 30 },
{ item: Items.IRON_SCIMITAR, name: 'Iron scimitar', price: 154, stock: 8 },
{ item: Items.OAK_SHORTBOW, name: 'Oak shortbow', price: 80, stock: 0 },
{ item: Items.ADAMANT_KITESHIELD, name: 'Adamant kiteshield', price: 5_440, stock: 2 },
];

.map() makes one component per entry:

src/shop.ts2
const BuyList = (): Component => (
<SunkenBox>
{STOCK.map((row, i) => (
<StockRow item={row.item} name={row.name} price={row.price} stock={row.stock} y={4 + i * ROW_H} />
))}
</SunkenBox>
);

.map() also runs over a list you declared or a table the game ships. Rendering lists covers the three kinds.

Pass a function to an event prop:

src/shop.ts2
<layer x={i * 79} y={0} w={74} h={22} onClick={() => { setTab(i); }}>

To make a component clickable, wrap it in a <layer> and put the handler there. Events lists every handler.

Anything the server owns is read with a hook:

src/shop.ts2
const CoinPurse = (): Component => {
const inventory = useInventory(Inventories.INVENTORY);
const coins = inventory.total(Items.COINS);

Game variables use useServerState, skills useSkill.

A cell holds state that is yours rather than the game’s, such as the open tab. Declare it at the top of the file when two components share it:

src/shop.ts2
const openTab = sessionState(0);

useState returns its value and a setter:

src/shop.ts2
const ShopContents = (): Component => {
const [tab] = useState(openTab);
return (
<layer x={0} y={28} w="fill" h={152}>
{tab === 0 && <BuyList />}
{tab === 1 && <SellList />}
{tab === 2 && <ShopInfo />}
</layer>
);
};

A cell for one component is useState(0). sessionState lasts until logout; persistedState is saved on the player’s device. Interface state has the details.

A <ui> returned from a function is what the game opens:

src/shop.ts2
const shopRef = createRef('docs-shop');
export const shopUi = (): UI => (
<ui ref={shopRef} w={252} h={260}>
<Shop />
</ui>
);

The createRef string is the interface’s name outside your code. w and h take the layer keywords, measured against the box it opens into. Interfaces and slots covers the rest.

Read the chapters in order from Writing markup, or look anything up in the Reference.