Guide
UI panels
A mod can put its own panels on the career hub: Home, your profile, Rankings, the Calendar, Finances and most other sections. A panel is data, not markup. You describe it with a small set of widgets (stat tiles, meters, tables, lists and so on) and the game draws it with its own components, so it looks like the rest of the game. It works with the mouse, the keyboard and a controller. Mods can't add HTML, CSS or JavaScript to the game's screens.
There are two ways to make a panel:
- From data: a JSON file in
data/ui/panels/. Its text can use{placeholders}that the game fills in from the career, and aconditiondecides when it shows. No script is needed. - From a script:
dtj.ui.panel(id, spec)in a script, usually in thepanelshook. The script works out what to show with the read-only queries.
The example-panels mod does both: a form guide on Home and a rivals table on your profile from its script, and a season card on Rankings from data.
A panel looks like the game's own, with no mod name on it. The Mods screen lists the loaded career's panels, with a switch for each to hide it or show it again. Hiding is kept per career. Switching a mod off on the Mods screen takes its panels off the hub.
Where panels go#
Each section with a mod slot has two places, set by placement:
"end"(the default) is after the section's own content."start"is above the section's own content.
In each place, panels sit side by side in a grid that runs the full width of the hub. The grid reflows to the window, about 22rem a column. "size": "wide" makes a panel take a whole row. Panels are sorted by order (−100 to 100, default 0, lower first), then by mod load order.
section | Screen |
|---|---|
home | Home |
calendar | Calendar |
bookings | Bookings (exhibitions) |
training | Training |
staff | Team |
player | Your profile (not other players' profiles) |
cards | Cards |
rankings | Rankings |
analytics | Analytics |
media | Media |
sponsors | Sponsors |
equipment | Equipment |
world | World |
finances | Finances |
legacy | Legacy |
The Inbox and News are split views, so they have no mod slots.
A panel#
| Field | Meaning |
|---|---|
id | Data panels only: a namespaced id like any record (acme.form.panel.season) |
section | Which screen (table above) |
placement | "start" or "end" (default) |
size | "normal" (default) or "wide" |
order | −100 to 100, default 0 |
title | Up to 60 characters |
subtitle | Optional, up to 120 |
condition | Data panels only: shown only while this holds (the story conditions) |
widgets | 1 to 8 widgets, drawn top to bottom |
Widgets#
Each widget has a type. tone is optional wherever it appears: good (green), bad (red), accent (the game's accent colour) or muted. A widget whose type is unknown is an error in data, and is dropped (with a warning in the script's log) from a script.
type | Fields | Caps |
|---|---|---|
text | text (a paragraph; line breaks kept), tone | 600 characters |
stats | items: { label, value, sub?, tone?, trend? }, where trend is up, down or flat and adds an arrow | 1–6 tiles; label 40, value 24 |
meter | label, value, max (default 100), display (text shown instead of the number), tone | label 40 |
keyValue | items: { label, value, tone?, link? } | 1–12 lines; value 80 |
table | columns: { label, align? } (align: "end" for numbers); rows: lists of cells; empty (shown when there are no rows) | 6 columns, 20 rows; cell 80 |
list | items: { text, sub?, meta?, tone?, link? } (meta is a short tag on the right, such as a date or a score); empty | 12 items; text 160, sub 120 |
chips | items: { text, tone? } | 12 chips of 24 characters |
bars | items: { label, value, display?, tone? }; max (default: the largest value) | 12 bars |
sparkline | values (numbers, oldest first), label | 2–60 points |
A table cell is text, a number, or { text, tone?, link? }. Numbers are shown in the player's language (12,345 or 12.345).
A list item with a player link shows that player's portrait, and one with an event link shows the event's logo and date.
An image widget (a picture from your mod's assets/) is planned. Panels already written won't need to change for it.
Links#
A link opens one of the game's own pages. It never goes to a web address.
{ "player": "<player id>" }opens that player's profile."me"is you.{ "event": "<event id>" }opens the event in the Event Centre. The id can be an id fromdtj.query.calendar()or an event template id (core.event.kingsgate-world-championship), which opens this season's running of that event.
A link to a player or event that doesn't exist is removed and the text stays. In data, the loader warns about it. From a script, it is dropped silently when the panel is shown.
Panels from data#
Put an array of panels in data/ui/panels.json or in any file under data/ui/panels/:
[
{
"id": "acme.form.panel.season",
"section": "rankings",
"placement": "start",
"title": "{first}'s season",
"subtitle": "{tier} · Order of Merit {rank}",
"condition": { "tierMax": 4 },
"widgets": [
{
"type": "stats",
"items": [
{ "label": "Record", "value": "{seasonWon}–{seasonLost}" },
{ "label": "Average", "value": "{seasonAverage}" },
{ "label": "Titles", "value": "{seasonTitles}", "tone": "accent" }
]
},
{ "type": "meter", "label": "Confidence", "value": "{confidence}" },
{
"type": "keyValue",
"items": [{ "label": "Rival", "value": "{rival}", "link": { "player": "{rival}" } }]
}
]
}
]Placeholders. Any text can use the placeholders that stories use: {name}, {first}, {nickname}, {money}, {rival}, {nextEvent}, {venue}, {city}, {sponsor}, {coach}, {agency}, {exhibition}, {town}, {dealSponsor}, {otherBidder} and {staff:<role>}. Panels can also use these:
| Placeholder | Is |
|---|---|
{date}, {year} | Today's date (YYYY-MM-DD) and the season |
{tier} | Your tier's name ("Pro Circuit") |
{rank}, {oomRank} | Order of Merit rank: #12 or —, and just 12 |
{cash}, {followers} | Your bank balance (£) and followers |
{confidence}, {morale}, {fatigue} | 0–100 |
{form} | −100 to 100 |
{load}, {streak} | Workload (0–100); your current run (3 = three wins in a row, −2 = two defeats) |
{seasonPlayed}, {seasonWon}, {seasonLost} | This season's matches |
{seasonAverage}, {season180s}, {seasonTitles}, {seasonCheckout} | This season's average, 180s, titles and checkout % |
A number field (a meter's value or max, a bar's value) can be a single placeholder, such as "{confidence}". If it doesn't come out as a number, that widget isn't shown. An unknown placeholder is left as written, and the loader warns about it. {me} and {rival} also work as link targets.
Conditions. condition takes the same fields as a story's condition, such as tierMax, rankMax, followersMin, months, hasRival and injured. They are checked each time the hub is shown. Conditions about a match that just ended (won, tv, major, againstRival, positionMax) never hold here, and chance is ignored.
Translating. A panel is content, so a translation patch changes its text. Put it in locale/<lang>/patches/, with "target": "panel:<id>". Widgets and their items are matched by position:
{
"target": "panel:acme.form.panel.season",
"patch": { "title": "{first}s Saison", "widgets": [{ "items": [{ "label": "Bilanz" }] }] }
}Data panels follow the installed mods, like stories and artwork. An existing career picks up new or changed panels the next time it loads.
Panels from a script#
dtj.on('panels', () => {
const season = dtj.query.seasonStats('me');
const last = dtj.query.matches('me', { limit: 5 });
dtj.ui.panel('form-guide', {
section: 'home',
title: 'Form guide',
size: 'wide',
widgets: [
{
type: 'stats',
items: [
{ label: 'Played', value: season ? season.matches : 0 },
{ label: 'Average', value: season && season.average ? season.average.toFixed(1) : '—' },
],
},
{
type: 'list',
empty: 'Your results will show here.',
items: last.map((m) => ({
text: (m.won ? 'Beat ' : 'Lost to ') + m.opponent,
meta: m.score ? m.score.join('–') : '',
tone: m.won ? 'good' : 'bad',
link: { player: m.opponentId },
})),
},
],
});
});| Call | Does |
|---|---|
dtj.ui.panel(id, spec) | Puts up the panel id (1–40 lowercase letters, digits and dashes), or replaces it. Returns a list of warnings (often empty) |
dtj.ui.remove(id) | Takes the panel away. Returns true if there was one |
dtj.ui.clear() | Takes all of this script's panels away. Returns how many there were |
spec is a panel without id or condition. Script text is shown exactly as written, and placeholders are not filled in, so build the text yourself. For text in the player's language, take it from your mod's own strings with dtj.t.
The checks are lenient where they can be:
- A list longer than its cap is cut to the cap.
- A widget that doesn't fit its schema is dropped.
- Each of these is returned as a warning and written to the script's log on the Mods screen.
The call throws a ScriptApiError, which you can catch, when the panel itself can't be used: a bad id, a missing title, a section without a slot, no usable widget left, or a ninth panel. A script can keep 8 panels, and can make **16 ui.panel calls in a hook call**.
The panels hook#
dtj.ui works in any hook. Put a "this week" panel in weekTick, or a "last match" panel in matchEnd. The panels hook is made for building panels:
- It runs when needed. It runs when a hub section is shown and something has changed since it last ran: a new day, one of your results, or any script hook running. On other renders the game shows the panels it already has. They are kept in the save, so a reloaded career shows them straight away.
- It only reads. All the queries work, but effects (
adjustDynamic,inbox,news) throw. Changes todtj.stateare thrown away, and its random numbers don't use up the career's. Looking at the hub never changes how a career plays out. - It has the same limits as any hook. It has no time limit, only the step limit for a stuck call (see Errors), and an error that isn't caught switches the script off.
ctxhasdate,yearandhumanId.
When a script is switched off (after an error), its panels go with it. When it is switched back on, they are built again.
A panel that needs something the panels hook can't work out by reading (last season's review, this week's column) can be prepared in another hook and kept in dtj.state: seasonEnd or weekTick write it, and panels draws it. The showcase mod does this for its Pundit's Corner on Home and its season review on Rankings, and builds a form guide on your profile from the queries alone (seasonStats, matches, headToHead and rivals).
What to check#
- Keep panels small: a title, a few widgets, and the key numbers. The hub is busy already.