Realms Divided (formerly Crown & Conquest), veryimportantmeeting.com edition: changes to the corresponding source
Applies to source.tar.gz (commit d01e38a6a386d02454175a8b95a6d0153217703f). From the source root: patch -p1 < source-changes.patch
Rebuild: npm ci, then GIT_COMMIT=d01e38a6a386d02454175a8b95a6d0153217703f npx webpack --config webpack.config.js --mode production (output in dist/client).
2026-09-28: territory capture 50% faster; towns +25% crowns and population; farms +25% grain and storage.
2026-09-28: Simple / Normal / Advanced realm modes (src/core/medieval/Modes.ts, home-screen picker, Single Player option).
2026-09-28: Back to homepage link on the game's home screen and a Home button (with confirm) in the battle top bar.
2026-09-28: Oak Workshop look with floating plaques (B2) for the battle HUD and home screen.
2026-09-28: Eleven more medieval maps (seven new close-up maps built by src/scripts/generateEditionMaps.ts from terrain PNGs made with the edition's map tools, plus four existing maps with period realm names) and the Council Hall home background.
Art, fonts, map data and images (resources/images/ui, resources/fonts/ui, resources/maps, resources/images/CouncilHall.webp) ship with the site and are credited on credits.html; they are not part of this text patch.
2026-09-28: game renamed Realms Divided (title, home heading, handbook, web app manifest).
2026-09-28: moved to veryimportantmeeting.com/realmsdivided/ (src/core/BasePath.ts prefixes every asset, map and API path; webpack publicPath; start-up script moved to resources/boot.js and PixiJS switched to its eval-free path for the site's Content-Security-Policy).
2026-09-28: live "playing now" counter on the home screen (markup in src/client/index.html; the script and styles are the site's shared /assets/live.js and live.css, the count comes from the site's Netlify function).
2026-09-28 (late): the page shows as soon as its HTML and scripts are ready (resources/boot.js waits for DOMContentLoaded instead of every image), and the live counter files carry ?v=2 so browsers fetch the fixed versions.
2026-10-01: the site's shared main menu (Start New Game, How to Play, Settings with Music and Effects volume, Feedback & Bugs, Credits, Exit) on the home screen: src/client/index.html loads /assets/menu.js and menu.css (shipped by the site, not part of the game) before the game scripts, and resources/vim-menu.js and vim-menu.css draw it into the home panel and press the game's own buttons (Single Player, Instructions, Settings), which they hide. No game code changed.
2026-10-01: Mistfall Studios credits roll (logo, Micah McCaul, Game Developer, artwork) at the top of resources/credits.html, which loads the site's shared /assets/mistfall/ files; the shared menu files are now loaded as ?v=2. No game code changed.
2026-10-23: larger controls, named building pictures and working medieval build shortcuts.
diff --git a/resources/boot.js b/resources/boot.js
new file mode 100644
--- /dev/null
+++ b/resources/boot.js
@@ -0,0 +1,42 @@
+// veryimportantmeeting.com edition (2026-09-28): the page's small start-up scripts,
+// moved out of index.html so the site's Content-Security-Policy (no inline scripts) allows them.
+(function () {
+ document.documentElement.className = "preload";
+
+ window.addEventListener("DOMContentLoaded", function () {
+ requestAnimationFrame(function () {
+ document.documentElement.classList.remove("preload");
+ });
+ });
+
+ // Adaptive scaling for .scale-container
+ function computeScale() {
+ var height = Math.max(
+ document.documentElement.clientHeight,
+ window.innerHeight || 0,
+ );
+ var baseHeight = 1150;
+ if (height >= baseHeight) return 1;
+ return Math.min(1, height / baseHeight);
+ }
+ function applyScale() {
+ var container = document.querySelector(".scale-container");
+ if (!container) return;
+ container.style.transform = "scale(" + computeScale() + ")";
+ // Compensate the layout gap the transform leaves behind.
+ container.style.marginBottom = "-440px";
+ }
+ function onResize() {
+ requestAnimationFrame(applyScale);
+ }
+ window.addEventListener("resize", onResize);
+ window.addEventListener("DOMContentLoaded", function () {
+ applyScale();
+ var close = document.getElementById("language-modal-close");
+ if (close)
+ close.addEventListener("click", function () {
+ document.getElementById("language-modal").classList.add("hidden");
+ });
+ });
+ onResize();
+})();
diff --git a/resources/credits.html b/resources/credits.html
--- a/resources/credits.html
+++ b/resources/credits.html
@@ -1 +1 @@
-
Crown & Conquest · Credits
Terratomic · Crown & Conquest
An independent adaptation of Terratomic, based on OpenFront. This edition is not operated by or affiliated with the original Terratomic team.
Great-hall background and medieval map illustrations: original AI-generated artwork for this edition. Crown & Conquest changes include independent HTTP lockstep multiplayer, private-room credentials, lobby creation, a quick-battle flow, portable hosting, replacement artwork, and deterministic medieval economy, siege, supply, research and vassal systems.
Multiplayer supports small private games with up to 8 sovereigns. The site itself is private unless its owner changes access. There is no connection to the original game's matchmaking, accounts, or global rankings. Games expire after six hours.
OpenFront and Terratomic contributors (original game art, CC BY-SA 4.0)
ToffeeCraft (wooden interface art)
Micah McCaul (the council hall painting)
85 1-bit Icons (artist not yet identified)
Mistfall Studios logo: graveyard art by CraftPix.net, title lettering in the Mana Seed font by Seliel the Shaper, wind sound by Chequered Ink, boom sound made with ElevenLabs.
Realms Divided
Realms Divided is veryimportantmeeting.com's edition of Crown & Conquest 1.0.0, renamed and changed for this site.
An independent adaptation of Terratomic, based on OpenFront. This edition is not operated by or affiliated with the original Terratomic team.
Wooden interface art for this edition: ToffeeCraft free UI packs. Pixel icons: a free 1-bit RPG icon pack. Fonts: Pixelify Sans and Jersey 10, SIL Open Font License 1.1. These art files are used here only and are not covered by the code license.
Council hall home background: artwork supplied by the site owner for this edition. Extra maps (Scotland, Wales, Ireland, the Narrow Sea, Holy Roman Empire, Scandinavia, the Aegean) were built for this edition from the game's own Britain and Europe terrain.
Great-hall background and medieval map illustrations: original AI-generated artwork for this edition. Crown & Conquest changes include independent HTTP lockstep multiplayer, private-room credentials, lobby creation, a quick-battle flow, portable hosting, replacement artwork, and deterministic medieval economy, siege, supply, research and vassal systems.
Multiplayer supports small private games with up to 8 sovereigns. The site itself is private unless its owner changes access. There is no connection to the original game's matchmaking, accounts, or global rankings. Games expire after six hours.
This independent game stores display preferences on your device. Temporary lobby credentials are stored for the current browser session. Multiplayer lobbies store player names, flags, hashed access tokens and game commands in the site's database. Saved single-player game records may include a randomly generated player identifier and game statistics.
This edition does not connect to Terratomic's accounts or matchmaking servers. Site access and hosting are provided through ChatGPT Sites. Do not use sensitive information as your player name.
\ No newline at end of file
+Privacy · Realms Divided
Privacy
This is the single-player edition of Realms Divided (based on Crown & Conquest), published on Very Important Meeting. There are no accounts, no multiplayer lobbies and no game server. Your settings and saved games stay in your browser's local storage and are not sent to us.
To show how many people are playing now, an open game page sends a random code, made up fresh for each visit, about once every 45 seconds. We keep only that code and the time, and forget it about two minutes after you leave.
We count visits with GoatCounter, a privacy-friendly counter that sets no cookies and does not follow you to other sites. Our host, Netlify, receives the standard request details every web server sees, such as your IP address and browser type.
\ No newline at end of file
diff --git a/resources/terms-of-service.html b/resources/terms-of-service.html
--- a/resources/terms-of-service.html
+++ b/resources/terms-of-service.html
@@ -1 +1 @@
-About · Crown & Conquest
About this edition
Crown & Conquest is an independent open-source game adaptation. It is not the official Terratomic service. Code is available under AGPL-3.0; the original openly licensed resources remain CC BY-SA 4.0. See the credits for full notices.
Games are provided as-is, without warranties. Military scenarios are fictional gameplay. Small private multiplayer games are supported; global matchmaking and official rankings are not connected.
\ No newline at end of file
+About · Realms Divided
About this edition
Realms Divided (based on Crown & Conquest) is an independent open-source game adaptation. It is not the official Terratomic service. Code is available under AGPL-3.0; the original openly licensed resources remain CC BY-SA 4.0. See the credits for full notices.
Games are provided as-is, without warranties. Military scenarios are fictional gameplay. Small private multiplayer games are supported; global matchmaking and official rankings are not connected.
\ No newline at end of file
diff --git a/resources/vim-menu.css b/resources/vim-menu.css
new file mode 100644
--- /dev/null
+++ b/resources/vim-menu.css
@@ -0,0 +1,192 @@
+/* veryimportantmeeting.com edition (2026-10-01): the shared site menu (/assets/menu.js) in the Oak Workshop look.
+ Drawn by vim-menu.js into the home panel where Single Player and Instructions were. */
+
+/* The menu replaces these on the home screen. They stay in the page so the menu can press them. */
+main #single-player,
+main #help-button,
+#settings-button,
+#vim-home-link {
+ display: none !important;
+}
+/* The row that held the (hidden) lobby buttons only added an empty gap. */
+main .container__row--equal:has(> #host-lobby-button) {
+ display: none !important;
+}
+/* Never show the menu during a battle. */
+.medieval-game #vim-menu {
+ display: none !important;
+}
+
+#vim-menu {
+ --vm-gap: 8px;
+ --vm-width: 100%;
+ --vm-accent: #8a4b1c;
+ --vm-muted: #3b1f0b;
+ --vm-danger: #8f1d12;
+ margin: 2px 0 6px;
+ color: #2b1a0d;
+ font-family: "Pixelify Sans", "Courier New", monospace;
+}
+#vim-menu .vm-list {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--vm-gap);
+}
+#vim-menu .vm-list > [data-vm="new"],
+#vim-menu .vm-list > [data-vm="exit"] {
+ grid-column: 1 / -1;
+}
+
+/* Oak plaque buttons, as the game's own home buttons. */
+.rd-vm-btn {
+ appearance: none;
+ margin: 0;
+ background: none;
+ border: 12px solid transparent;
+ border-image: url("/realmsdivided/images/ui/oak-panel-sm.png") 14 fill / 18px stretch;
+ image-rendering: pixelated;
+ color: #fff4da;
+ font: 700 16px/1.15 "Pixelify Sans", "Courier New", monospace;
+ letter-spacing: 0.5px;
+ text-shadow:
+ 2px 2px 0 #3b1f0b,
+ -1px 0 0 #3b1f0b;
+ padding: 2px 8px;
+ cursor: pointer;
+ filter: drop-shadow(0 2px 0 #3b1f0b55);
+ transition: filter 120ms ease-out;
+}
+#vim-menu .rd-vm-btn.vm-item {
+ min-height: 44px;
+}
+.rd-vm-btn:hover {
+ filter: brightness(1.1) drop-shadow(0 2px 0 #3b1f0b55);
+}
+.rd-vm-btn:active {
+ filter: brightness(0.95);
+}
+.rd-vm-btn:focus-visible {
+ outline: 2px solid #2b1a0d;
+ outline-offset: 2px;
+}
+#vim-menu .vm-list > [data-vm="new"] {
+ min-height: 54px;
+}
+#vim-menu .vm-list > [data-vm="new"] .vm-label {
+ font-size: 21px;
+}
+.rd-vm-btn .vm-sub {
+ font: 400 11.5px/1.2 "Pixelify Sans", "Courier New", monospace;
+ color: #2b1a0d;
+ text-shadow: none;
+ opacity: 1;
+ margin-top: 2px;
+}
+
+/* Settings and Credits panels sit on the same darker inset as the realm mode picker. */
+#vim-menu .vm-panel {
+ background: #3b1f0b33;
+ border-radius: 6px;
+ padding: 8px 10px 10px;
+}
+#vim-menu .vm-h {
+ font: 400 30px/1 "Jersey 10", "Pixelify Sans", monospace;
+ letter-spacing: 1px;
+ color: #fff4da;
+ text-shadow:
+ 2px 2px 0 #3b1f0b,
+ -1px 0 0 #3b1f0b,
+ 0 -1px 0 #3b1f0b;
+ margin: 2px 0 4px;
+}
+#vim-menu .vm-slider {
+ font: 700 15px "Pixelify Sans", "Courier New", monospace;
+ color: #2b1a0d;
+}
+/* Pixel-style sliders (the game's base styles strip the browser's own look). */
+#vim-menu .vm-slider input[type="range"] {
+ -webkit-appearance: none;
+ appearance: none;
+ height: 28px;
+ background: transparent;
+}
+#vim-menu .vm-slider input[type="range"]::-webkit-slider-runnable-track {
+ height: 10px;
+ background: #3b1f0b66;
+ border: 2px solid #3b1f0b;
+}
+#vim-menu .vm-slider input[type="range"]::-moz-range-track {
+ height: 6px;
+ background: #3b1f0b66;
+ border: 2px solid #3b1f0b;
+}
+#vim-menu .vm-slider input[type="range"]::-moz-range-progress {
+ height: 6px;
+ background: #fff4da;
+}
+#vim-menu .vm-slider input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 16px;
+ height: 24px;
+ margin-top: -9px;
+ background: #fff4da;
+ border: 3px solid #3b1f0b;
+ border-radius: 0;
+ cursor: pointer;
+}
+#vim-menu .vm-slider input[type="range"]::-moz-range-thumb {
+ width: 10px;
+ height: 18px;
+ background: #fff4da;
+ border: 3px solid #3b1f0b;
+ border-radius: 0;
+ cursor: pointer;
+}
+#vim-menu .vm-slider input[type="range"]:focus-visible {
+ outline: 2px solid #2b1a0d;
+ outline-offset: 2px;
+}
+#vim-menu .vm-extra {
+ min-height: 42px;
+ font-size: 15px;
+}
+#vim-menu .vm-credits .vm-maker {
+ font: 400 34px/1 "Jersey 10", "Pixelify Sans", monospace;
+ letter-spacing: 1px;
+ color: #fff4da;
+ text-shadow:
+ 2px 2px 0 #3b1f0b,
+ -1px 0 0 #3b1f0b,
+ 0 -1px 0 #3b1f0b;
+ font-weight: 400;
+}
+#vim-menu .vm-credits .vm-role {
+ font: 700 14px "Pixelify Sans", "Courier New", monospace;
+ letter-spacing: 1.5px;
+ text-transform: uppercase;
+ color: #2b1a0d;
+ opacity: 1;
+}
+#vim-menu .vm-note {
+ font: 13px/1.4 "Pixelify Sans", "Courier New", monospace;
+ color: #2b1a0d;
+ opacity: 1;
+}
+#vim-menu .vm-note a {
+ color: #2b1a0d;
+ font-weight: 700;
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+#vim-menu .vm-note a:hover {
+ color: #5a3414;
+}
+
+@media (max-width: 600px) {
+ .rd-vm-btn {
+ font-size: 14px;
+ letter-spacing: 0;
+ padding: 2px 2px;
+ }
+}
diff --git a/resources/vim-menu.js b/resources/vim-menu.js
new file mode 100644
--- /dev/null
+++ b/resources/vim-menu.js
@@ -0,0 +1,121 @@
+// veryimportantmeeting.com edition (2026-10-01): the site's shared main menu on the Realms Divided home screen.
+// A plain same-origin script (the site's Content-Security-Policy allows no inline scripts). It does not touch the
+// game code: it draws /assets/menu.js (window.VIMMenu) into the oak home panel and works the game's own buttons,
+// which stay in the page but hidden (vim-menu.css):
+// Start New Game -> #single-player (map, rivals and realm mode setup)
+// How to Play -> #help-button
+// Settings -> Music and Effects sliders, plus "More game settings" -> #settings-button (the game's own panel)
+// Battles run as a simulation in a web worker with no way to save and resume, so Continue and Load Game are left out.
+// The "Claim your crown" card and its realm mode picker stay above the menu as they were.
+(function () {
+ "use strict";
+ var ROOT_ID = "vim-menu";
+
+ function byId(id) {
+ return document.getElementById(id);
+ }
+ function press(id) {
+ var el = byId(id);
+ if (el) el.click();
+ }
+ function inBattle() {
+ return document.body.classList.contains("medieval-game");
+ }
+
+ function options(root) {
+ return {
+ root: root,
+ buttonClass: "rd-vm-btn",
+ backClass: "rd-vm-btn rd-vm-back",
+ items: { continue: false, load: false },
+ newGameNote: "Choose a map, rivals and realm mode",
+ extras: [{ label: "How to Play", onClick: function () { press("help-button"); } }],
+ settingsExtras: [{ label: "More game settings", onClick: function () { press("settings-button"); } }],
+ credits: { url: "/realmsdivided/credits.html", linkText: "Art, music, maps and license credits" },
+ exitUrl: "/",
+ onView: refit,
+ onNewGame: function () { press("single-player"); }
+ };
+ }
+
+ // Put the menu where the old Single Player and Instructions buttons were.
+ function place(root) {
+ var anchor = byId("single-player");
+ if (anchor && anchor.parentNode) {
+ anchor.parentNode.insertBefore(root, anchor);
+ return true;
+ }
+ var panel = document.querySelector("main .submarine-panel");
+ if (panel) {
+ panel.appendChild(root);
+ return true;
+ }
+ return false;
+ }
+
+ function ensure() {
+ if (!window.VIMMenu) return;
+ var root = byId(ROOT_ID);
+ if (root && root.isConnected && root.querySelector(".vm-list, .vm-panel")) return;
+ if (!root) {
+ root = document.createElement("div");
+ root.id = ROOT_ID;
+ }
+ if (!root.isConnected && !place(root)) return;
+ window.VIMMenu.mount(options(root));
+ }
+
+ // The home panel is scaled down to fit the window (boot.js). On short screens its bottom can end up behind the
+ // fixed footer with no room to scroll; give the page just enough extra room to scroll it clear.
+ var fitTimer = 0;
+ function fit() {
+ fitTimer = 0;
+ var main = document.querySelector("main");
+ var panel = document.querySelector("main .submarine-panel");
+ if (!main || !panel) return;
+ main.style.paddingBottom = "";
+ if (inBattle()) return;
+ var footer = document.querySelector("footer.l-footer");
+ for (var i = 0; i < 2; i++) {
+ var need = panel.getBoundingClientRect().bottom + window.scrollY + (footer ? footer.offsetHeight : 0) + 12;
+ var short = need - document.documentElement.scrollHeight;
+ if (short <= 1) break;
+ main.style.paddingBottom = (parseFloat(getComputedStyle(main).paddingBottom) || 0) + Math.ceil(short) + "px";
+ }
+ }
+ function refit() {
+ if (!fitTimer) fitTimer = setTimeout(function () { requestAnimationFrame(fit); }, 60);
+ }
+
+ function start() {
+ ensure();
+ refit();
+ window.addEventListener("resize", refit);
+ window.addEventListener("load", refit);
+ if (document.fonts && document.fonts.ready) document.fonts.ready.then(refit);
+ var wasBattle = inBattle();
+ // Leaving a battle (or the panel being redrawn) brings the menu back on its main view.
+ new MutationObserver(function () {
+ var now = inBattle();
+ if (wasBattle && !now) {
+ ensure();
+ if (window.VIMMenu) window.VIMMenu.show("main");
+ }
+ refit();
+ wasBattle = now;
+ }).observe(document.body, { attributes: true, attributeFilter: ["class"] });
+ var main = document.querySelector("main");
+ if (main) {
+ new MutationObserver(function () {
+ var root = byId(ROOT_ID);
+ if (!root || !root.isConnected) ensure();
+ }).observe(main, { childList: true, subtree: true });
+ }
+ }
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", function () { setTimeout(start, 0); });
+ } else {
+ setTimeout(start, 0);
+ }
+})();
diff --git a/src/client/CommandLobby.ts b/src/client/CommandLobby.ts
--- a/src/client/CommandLobby.ts
+++ b/src/client/CommandLobby.ts
@@ -1,3 +1,4 @@
+import { BASE_PATH } from "../core/BasePath";
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { Difficulty, GameMapType, GameMode, GameType } from "../core/game/Game";
@@ -9,6 +10,7 @@
import "./command.css";
import type { FlagInput } from "./FlagInput";
import type { UsernameInput } from "./UsernameInput";
+import { currentRealmMode } from "./RealmModePicker";
import { getMapsImage } from "./utilities/Maps";
export function identity() {
@@ -25,6 +27,7 @@
export function defaultConfig() {
return {
medieval: true,
+ medievalMode: currentRealmMode(),
gameMap: GameMapType.Europe,
difficulty: Difficulty.Medium,
gameMode: GameMode.FFA,
@@ -137,7 +140,7 @@
this.token = crypto.randomUUID();
if (this.mode === "host") {
this.code = generateID();
- await this.api("/api/rooms", "POST", {
+ await this.api(BASE_PATH + "/api/rooms", "POST", {
id: this.code,
clientID: this.clientID,
token: this.token,
@@ -145,7 +148,7 @@
config: this.config,
});
} else {
- await this.api(`/api/rooms/${this.code}/join`, "POST", {
+ await this.api(`${BASE_PATH}/api/rooms/${this.code}/join`, "POST", {
clientID: this.clientID,
token: this.token,
...identity(),
@@ -164,7 +167,7 @@
}
private async refresh() {
try {
- const room = await this.api(`/api/rooms/${this.code}`);
+ const room = await this.api(`${BASE_PATH}/api/rooms/${this.code}`);
this.room = room;
this.error = "";
if (room.startedAt && !this.launchPending) {
@@ -186,7 +189,7 @@
private async start() {
this.busy = true;
try {
- await this.api(`/api/rooms/${this.code}/start`, "POST", {});
+ await this.api(`${BASE_PATH}/api/rooms/${this.code}/start`, "POST", {});
await this.refresh();
} catch (e) {
this.error = (e as Error).message;
diff --git a/src/client/FlagInput.ts b/src/client/FlagInput.ts
--- a/src/client/FlagInput.ts
+++ b/src/client/FlagInput.ts
@@ -1,3 +1,4 @@
+import { BASE_PATH } from "../core/BasePath";
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { FlagSchema } from "../core/Schemas";
@@ -103,7 +104,7 @@
class="border p-[4px] rounded-lg flex cursor-pointer border-black/30 dark:border-gray-300/60 bg-white/70 dark:bg-[rgba(55,65,81,0.7)]"
title="Pick a flag!"
>
-
+
${this.showModal
? html`
@@ -139,7 +140,7 @@
>
${country.name}
diff --git a/src/client/HelpModal.ts b/src/client/HelpModal.ts
--- a/src/client/HelpModal.ts
+++ b/src/client/HelpModal.ts
@@ -1,3 +1,4 @@
+import { BASE_PATH } from "../core/BasePath";
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
@@ -373,21 +374,21 @@
{
titleKey: "ui_guide.command_center_build_title",
descKey: "ui_guide.command_center_build_desc",
- img: "/images/HelpModalScreenshots/CC-Build.png",
+ img: BASE_PATH + "/images/HelpModalScreenshots/CC-Build.png",
altKey: "ui_guide.command_center_build_alt",
icon: ``,
},
{
titleKey: "ui_guide.command_center_attack_title",
descKey: "ui_guide.command_center_attack_desc",
- img: "/images/HelpModalScreenshots/CC-Attack.png",
+ img: BASE_PATH + "/images/HelpModalScreenshots/CC-Attack.png",
altKey: "ui_guide.command_center_attack_alt",
icon: ``,
},
{
titleKey: "ui_guide.command_center_economy_title",
descKey: "ui_guide.command_center_economy_desc",
- img: "/images/HelpModalScreenshots/CC-Economoy.png",
+ img: BASE_PATH + "/images/HelpModalScreenshots/CC-Economoy.png",
altKey: "ui_guide.command_center_economy_alt",
icon: ``,
},
@@ -444,7 +445,7 @@
${this.t("ui_guide.welcome_title")}
@@ -472,7 +473,7 @@
-
Crown & Conquest
+
Realms Divided
Build a kingdom that can feed its people and withstand its enemies.
+
+
Realm modes
+
+ Pick a mode under the Claim your crown button or in Single Player.
+ Simple: build towns, trading posts, harbors and castles only.
+ Towns give 50% more crowns and population, and trade gives 25% more.
+ There is no grain, equipment, recruiting, workers, exhaustion, unrest
+ or research, and taxes stay at 20%. Your whole population fights.
+ Normal: no equipment, smithies or war exhaustion, and every
+ production building (towns, farms, trading posts, harbors,
+ monasteries and universities) yields 25% more. Advanced: every
+ system in this handbook, as the game was first made.
Claim land
diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts
--- a/src/client/InputHandler.ts
+++ b/src/client/InputHandler.ts
@@ -1,3 +1,5 @@
+import { BUILD_HOTKEYS, BUILDINGS, type HoldingKind } from "../core/medieval/Medieval";
+import { modeRules } from "../core/medieval/Modes";
import { EventBus, GameEvent } from "../core/EventBus";
import { TileRef } from "../core/game/GameMap";
import { UnitView } from "../core/game/GameView";
@@ -337,6 +339,21 @@
}
});
window.addEventListener("keyup", (e) => {
+ if (this.game.config().gameConfig().medieval) {
+ const editable = e.composedPath().some((el) => el instanceof HTMLElement &&
+ (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName)));
+ if (editable || e.ctrlKey || e.altKey || e.metaKey) return;
+ const choice = Object.entries(BUILD_HOTKEYS).find(([, key]) => e.code === `Key${key}`);
+ if (choice || e.code === "Escape") {
+ e.preventDefault();
+ const kind = choice?.[0] as HoldingKind | undefined;
+ const player = this.game.myPlayer();
+ if (!kind || (player && !this.game.inSpawnPhase() && modeRules(this.game).buildings.includes(kind) && player.gold() >= BigInt(BUILDINGS[kind].cost))) {
+ document.dispatchEvent(new CustomEvent("choose-medieval-building", { detail: { kind: kind ?? null } }));
+ }
+ return;
+ }
+ }
if (e.code === this.keybinds.toggleView) {
e.preventDefault();
this.alternateView = false;
diff --git a/src/client/LangSelector.ts b/src/client/LangSelector.ts
--- a/src/client/LangSelector.ts
+++ b/src/client/LangSelector.ts
@@ -1,3 +1,4 @@
+import { BASE_PATH } from "../core/BasePath";
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import "./LanguageModal";
@@ -227,7 +228,7 @@
"lang-selector",
];
- document.title = this.translateText("main.title") ?? document.title;
+ // veryimportantmeeting.com edition (2026-09-28): keep the page title "Realms Divided" in every language.
document.querySelectorAll("[data-i18n]").forEach((element) => {
const key = element.getAttribute("data-i18n");
@@ -319,7 +320,7 @@
${currentLang.native} (${currentLang.en})
diff --git a/src/client/LanguageModal.ts b/src/client/LanguageModal.ts
--- a/src/client/LanguageModal.ts
+++ b/src/client/LanguageModal.ts
@@ -1,3 +1,4 @@
+import { BASE_PATH } from "../core/BasePath";
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { translateText } from "../client/Utils";
@@ -95,7 +96,7 @@
@click=${() => this.selectLanguage(lang.code)}
>
diff --git a/src/client/LobbyWatcher.ts b/src/client/LobbyWatcher.ts
--- a/src/client/LobbyWatcher.ts
+++ b/src/client/LobbyWatcher.ts
@@ -1,3 +1,4 @@
+import { BASE_PATH } from "../core/BasePath";
import { EventBus } from "../core/EventBus";
import { UserSettings } from "../core/game/UserSettings";
import { GameInfo } from "../core/Schemas";
@@ -43,7 +44,7 @@
if (this.isMuted()) return;
try {
- const response = await fetch("/api/public_lobbies");
+ const response = await fetch(BASE_PATH + "/api/public_lobbies");
if (!response.ok) return;
const data = await response.json();
const lobbies: GameInfo[] = data.lobbies;
diff --git a/src/client/Main.ts b/src/client/Main.ts
--- a/src/client/Main.ts
+++ b/src/client/Main.ts
@@ -1,4 +1,8 @@
+// veryimportantmeeting.com edition (2026-09-28): the site CSP forbids eval, so PixiJS uses its eval-free shader path.
+import "pixi.js/unsafe-eval";
+import { BASE_PATH } from "../core/BasePath";
import { openCommandLobby, quickBattle } from "./CommandLobby";
+import { mountHomeLink, mountRealmModePicker } from "./RealmModePicker";
import { EventBus } from "../core/EventBus";
import { GameRecord, GameStartInfo, ID } from "../core/Schemas";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
@@ -433,7 +437,7 @@
try {
// Avoid creating multiple elements
if (!this.menuMusic) {
- const audio = new Audio("/music/festival-march.mp3");
+ const audio = new Audio(BASE_PATH + "/music/festival-march.mp3");
audio.loop = true;
audio.preload = "auto";
audio.volume = 0.35; // pleasant background level
@@ -640,6 +644,8 @@
document.addEventListener("DOMContentLoaded", () => {
new Client().initialize();
document.getElementById("quick-battle")?.addEventListener("click", quickBattle);
+ mountRealmModePicker();
+ mountHomeLink();
});
// WARNING: DO NOT EXPOSE THIS ID
diff --git a/src/client/MedievalCouncil.ts b/src/client/MedievalCouncil.ts
--- a/src/client/MedievalCouncil.ts
+++ b/src/client/MedievalCouncil.ts
@@ -1,3 +1,4 @@
+import { BASE_PATH } from "../core/BasePath";
import { LitElement, css, html, nothing } from "lit";
import { EventBus } from "../core/EventBus";
import { Cell, UnitType } from "../core/game/Game";
@@ -13,6 +14,7 @@
armyStats,
constructionProgress,
} from "../core/medieval/Medieval";
+import { buildingDescription, modeRules } from "../core/medieval/Modes";
import { GALLEY_LENGTH } from "../core/medieval/NavalCombat";
import { MouseMoveEvent, MouseUpEvent } from "./InputHandler";
import "./MedievalResearch";
@@ -578,19 +580,22 @@
- Castles defend land and support population and trade. Farms feed the
- host; smithies forge equipment.
+ Castles defend land and support population and trade.
+ ${modeRules(this.game).food ? "Farms feed the host." : ""}
+ ${modeRules(this.game).equipment ? "Smithies forge equipment." : ""}
Each purchase recruits up to 500 workers. Stronger soldiers add more
attack power per person. The counts below separate troops at home from
- deployed troops; casualties reduce your companies. Food and exhaustion
- affect battle strength.
+ deployed troops; casualties reduce your companies.
+ ${rules.exhaustion
+ ? "Food and exhaustion affect battle strength."
+ : "Food affects battle strength."}
Upkeep: ${scholarlyUpkeep(s, this.game.ticks())} crowns/s for
- monasteries and universities. Base 1 · +0.5 per monastery · +1.5 per
- university · Governance
+ monasteries and universities. Base 1 ·
+ +${0.5 * modeRules(this.game).productionMul} per monastery ·
+ +${1.5 * modeRules(this.game).productionMul} per university ·
+ Governance
+${(s.research.governance * RESEARCH_INCREMENT * 100).toFixed(2)}%