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 a condition decides when it shows. No script is needed.
  • From a script: dtj.ui.panel(id, spec) in a script, usually in the panels hook. 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.

sectionScreen
homeHome
calendarCalendar
bookingsBookings (exhibitions)
trainingTraining
staffTeam
playerYour profile (not other players' profiles)
cardsCards
rankingsRankings
analyticsAnalytics
mediaMedia
sponsorsSponsors
equipmentEquipment
worldWorld
financesFinances
legacyLegacy

The Inbox and News are split views, so they have no mod slots.

A panel#

FieldMeaning
idData panels only: a namespaced id like any record (acme.form.panel.season)
sectionWhich screen (table above)
placement"start" or "end" (default)
size"normal" (default) or "wide"
order−100 to 100, default 0
titleUp to 60 characters
subtitleOptional, up to 120
conditionData panels only: shown only while this holds (the story conditions)
widgets1 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.

typeFieldsCaps
texttext (a paragraph; line breaks kept), tone600 characters
statsitems: { label, value, sub?, tone?, trend? }, where trend is up, down or flat and adds an arrow1–6 tiles; label 40, value 24
meterlabel, value, max (default 100), display (text shown instead of the number), tonelabel 40
keyValueitems: { label, value, tone?, link? }1–12 lines; value 80
tablecolumns: { label, align? } (align: "end" for numbers); rows: lists of cells; empty (shown when there are no rows)6 columns, 20 rows; cell 80
listitems: { text, sub?, meta?, tone?, link? } (meta is a short tag on the right, such as a date or a score); empty12 items; text 160, sub 120
chipsitems: { text, tone? }12 chips of 24 characters
barsitems: { label, value, display?, tone? }; max (default: the largest value)12 bars
sparklinevalues (numbers, oldest first), label2–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.

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 from dtj.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:

PlaceholderIs
{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 },
        })),
      },
    ],
  });
});
CallDoes
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 to dtj.state are 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. ctx has date, year and humanId.

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.

Written for Darts Player Simulator 0.50.32. Built from the game's own schemas and its base pack.