Mod Screens (WebUI)

Fall of an Empire is, by some measure, mostly user interface, and that whole interface is a React and TypeScript app rendered inside the game. That works out nicely for modding: rather than wrestling with engine UI tooling, you write TSX the same way you'd write it for any web app, and it shows up in the game. This page is about building your own screens, sidebars and toolbar buttons.

What the UI is

The game's interface is a web front-end (React, TypeScript, CSS) hosted inside the game through an embedded browser. It talks to the C++/AngelScript game over a bridge. It's surprisingly performant, and because it's just web tech, a mod can add entire new panels and systems with nothing but a React package and some assets.

The full source of the base game's UI is public - fall-of-an-empire-ui - and it's the best reference there is for how everything fits together. Your mod screens sit alongside that code and use the same SDK and bridge.

The package

A mod's WebUI is a React package inside your content pack. You write the source; a build step produces the JavaScript, CSS, and optimised assets that the game actually loads.

Files
Mods/
└── YourModName/
├── mod.json
└── WebUI/
├── package.json
├── vite.config.ts
├── src/your TSX, CSS, and source PNG/JPG
├── public/
│ └── assets/source images
├── Localization/
│ ├── en.po
│ └── de.po
└── dist/build output - do not edit by hand

You edit src/ and public/. You never edit dist/ - the build regenerates it, including converting your source images to WebP.

mod.json points at the built files:

JSON
"webui": {
  "localization": "WebUI/Localization/{locale}.po",
  "entries": [
    { "id": "your_mod_screen", "script": "WebUI/dist/index.js" }
  ],
  "styles": ["WebUI/dist/style.css"]
}

The SDK

The host publishes a stable SDK you build your mod against. It's available two ways: on globalThis.FOAE, or as an import from @foae/sdk (map that import to /sdk/foae-sdk.js in your Vite config). The full TypeScript declaration ships with the game at WebUI/public/sdk/foae-sdk.d.ts.

The SDK gives you three kinds of thing.

1. Registration - getting into the game

You add UI by registering it. Three entry points:

TSX
const { registry } = globalThis.FOAE;

registry.registerScreen({ id, render });          // a full screen
registry.registerSidebar({ id, side, component }); // a left/right panel
registry.registerTopbarButton({ id, label, icon }); // a button on the top bar

A minimal screen with a button that opens it looks like this:

TSX
import type { FoaeModSDK } from "@foae/sdk";

const FOAE = globalThis.FOAE as FoaeModSDK;
const { registry, components, hooks, localization } = FOAE;
const { ScreenShell, SectionHeading, Panel } = components;

function CommandScreen({ onClose }: { onClose: () => void }) {
  const state = hooks.useGameState();
  const playerId = hooks.usePlayerFactionId();
  const player = hooks.useFaction(playerId);

  return (
    <ScreenShell title={localization.t("Sicily.Command.Title")} onClose={onClose}>
      <SectionHeading title={player?.name ?? ""} />
      <Panel>Treasury: {state.gold}</Panel>
    </ScreenShell>
  );
}

registry.registerScreen({
  id: "sicily_command",
  render: ({ onClose }) => <CommandScreen onClose={onClose} />,
});

registry.registerTopbarButton({
  id: "sicily_command_btn",
  label: localization.t("Sicily.Command.Button"),
  icon: "command",
  placement: "right",
});

The id you register a screen with is the entries[].id in mod.json, and it's what a topbar button's openedByTopbar / bridge wiring refers to. Registrations also take useful extras - factionMode to show a panel only when playing as a certain kind of faction, preloadAssets to warm images, advisorTopic, and so on.

2. Components and hooks - building the screen

The SDK hands you the same building blocks the base UI is made from. The components include ScreenShell (the framed window with a title bar and close button) along with SectionHeading, Panel, GameButton, GameCard, CloseButton, Tooltip and InfoRow, and using them means your screen matches the rest of the game without any effort. For data there are hooks: useGameState() for the live game state (date, gold, population, speed and so on), usePlayerFactionId(), and useFaction(id), usePerson(id), useSettlement(id) and useMilitary(id) for individual entities. These are reactive, so when the game updates your component re-renders on its own. And for driving the UI, useGameActions() gives you openScreen, openSidebar, showAdvisor and the like.

3. The bridge - talking to gameplay

For anything beyond reading standard state - running your own logic, mutating the game - you call a bridge action that you wrote in AngelScript (see Hooking Into Game Systems):

TSX
const { bridge } = FOAE;

// call an action your UBridgeAction handles, and await the response
const result = await bridge.call("game.delete_formation_template", {
  templateId: id,
});

// listen for events your script pushes to the UI
const off = bridge.on("ui.sidebar_event", (data) => {
  // react to something that happened in the game
});
// call off() to stop listening

bridge.call is the request/response half; bridge.on is the push half. Together with the UBridgeAction on the script side, that's the entire conversation between your screen and the game.

Localisation

Mod UI text goes through PO catalogues, declared by webui.localization in mod.json with a {locale} placeholder. The runtime loads en, then the base locale, then the current locale, falling back to English for anything a translation is missing. The stable key is the msgctxt:

PO
msgctxt "Sicily.Command.Title"
msgid "Command"
msgstr "Command"

Read it back in code through the SDK - localization.t("Sicily.Command.Title"), or with interpolation localization.t("Sicily.Command.Count", { Count: 3 }).

Mock mode

You don't need to launch the game every time you tweak a screen. The WebUI runs in the browser against a mock bridge that returns fixture data, so you get an instant Vite dev loop:

PowerShell
npm.cmd run dev:mock

Open the printed URL. A launcher appears bottom-right for opening registered screens, sidebars, events, and modals. You can also jump straight to a state with query params:

Text
http://localhost:5173/?mock=1&screen=economy
http://localhost:5173/?mock=1&sidebar=settlement&id=mock-settlement-capital
http://localhost:5173/?mock=1&mode=mainmenu

The mock only installs in Vite dev mode (with --mode mock, VITE_FOAE_MOCK_UI=1, or ?mock=1). Production builds always use the real runtime bridge.

Debugging in the real game: the CEF DevTools

The mock catches most problems, but eventually you'll want to see your screen running against the real bridge, real data, and the real renderer. The game's UI is rendered through FoaeCefUI, an embedded Chromium browser (CEF), and it ships with a DevTools inspector you can open in-game.

Open the game's console (the ~ / tilde key) and run:

Text
foae.GameUI.OpenDevTools

That opens a separate FoaeCefUI Inspector window - a version of Chromium DevTools, so you get the Elements, Console, Network, Performance, and Memory tabs you already know. Inspect your screen's DOM, read console.log output from your mod code, set breakpoints, and profile a slow panel with the Performance tab exactly as you would in a browser. If the UI happens to be disabled when you run the command, it enables and mounts it first.

A couple of things are deliberately different from a normal Chrome inspector: the inspector is a bundled, patched DevTools frontend: it's served from files shipped inside the game. It's also locked down, with external resource requests from the frontend blocked so that nothing phones home.

This is what you'll want when a screen renders fine in mock mode but misbehaves in the real game, when a bridgeCall isn't returning what you expected, or when you just want to confirm a panel isn't dropping frames. Because the game UI is web tech through and through, more or less everything you'd normally do in browser DevTools works here too.

Building

Your mod's WebUI/ folder is a Vite package. From inside that folder:

PowerShell
cd Mods/YourModName/WebUI
npm install
npm run build

That produces the dist/ files mod.json points at (WebUI/dist/index.js and WebUI/dist/style.css). Build it before you load the mod in the game - the webui block points at these built files, so the screen won't appear until they exist.

Next