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.

Original game code © 2020–2025 OpenFront contributors. Modifications © 2025 Terratomic contributors. Code distributed under the GNU Affero General Public License, version 3.

Download the complete corresponding source · Read the code license

Original resources © 2020–2025 OpenFront contributors and © 2025 Terratomic contributors, used under CC BY-SA 4.0. Restricted assets from the upstream proprietary folder are excluded. Replacement unit symbols use the openly licensed resources.

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.

Return to command

+Realms Divided · Credits

Micah McCaul

Game Developer, Realms Divided edition

Artwork

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.

Original game code © 2020–2025 OpenFront contributors. Modifications © 2025 Terratomic contributors. Code distributed under the GNU Affero General Public License, version 3.

Download the complete corresponding source plus this edition's changes (faster territory capture, stronger towns and farms, Simple / Normal / Advanced realm modes, larger building controls and keyboard shortcuts) · Read the code license

Original resources © 2020–2025 OpenFront contributors and © 2025 Terratomic contributors, used under CC BY-SA 4.0. Restricted assets from the upstream proprietary folder are excluded. Replacement unit symbols use the openly licensed resources.

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.

Back to the game · Back to homepage

diff --git a/resources/manifest.json b/resources/manifest.json --- a/resources/manifest.json +++ b/resources/manifest.json @@ -3,13 +3,13 @@ { "purpose": "maskable", "sizes": "512x512", - "src": "/icons/icon512_maskable.png", + "src": "/realmsdivided/icons/icon512_maskable.png", "type": "image/png" }, { "purpose": "any", "sizes": "512x512", - "src": "/icons/icon512_rounded.png", + "src": "/realmsdivided/icons/icon512_rounded.png", "type": "image/png" } ], @@ -21,8 +21,8 @@ "display": "standalone", "dir": "auto", "lang": "en", - "name": "Crown & Conquest", - "short_name": "Crown", - "start_url": "/", - "id": "/" + "name": "Realms Divided", + "short_name": "Realms", + "start_url": "/realmsdivided/", + "id": "/realmsdivided/" } \ No newline at end of file diff --git a/resources/privacy-policy.html b/resources/privacy-policy.html --- a/resources/privacy-policy.html +++ b/resources/privacy-policy.html @@ -1 +1 @@ -Privacy · Crown & Conquest

Privacy

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.

Return to command · Credits and source

\ 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.

The full policy is at veryimportantmeeting.com/privacy-policy.html. This edition does not connect to Terratomic's accounts or matchmaking servers.

Back to the game · Credits and source · Back to homepage

\ 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.

Return to command · Credits and source

\ 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.

Back to the game · Credits and source · Back to homepage

\ 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")} ${this.t( @@ -472,7 +473,7 @@
${this.t(
${this.t(
${this.t(
${this.t(
${this.t(
${this.t( ${this.t( ${this.t( ${this.t( ${this.t(
${this.t(
${this.t(
${this.t(
-

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 @@ flag ${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)} > ${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 @@

${Object.entries(BUILDINGS) - .filter( - ([kind]) => - !["road", "bridge", "trebuchet", "catapult"].includes(kind), + .filter(([kind]) => + modeRules(this.game).buildings.includes(kind as HoldingKind), ) .map( ([kind, b]) => html`

- 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." : ""}

`; } private armyView(p: PlayerView) { const s = p.data.medieval!; + const rules = modeRules(this.game); const army = armyStats(s, p.troops(), p.data.attackingTroops), recruits = Math.min(500, Math.floor(p.workers() * 0.15)); return html`

The royal host

@@ -627,7 +634,15 @@ soldier. Attacks draw a proportional mix from your ready army.

-
+ ${rules.recruitment + ? nothing + : html`

+ ${rules.label} mode has no recruiting. Your whole population serves + in the host and it grows on its own as your land, towns and castles + grow. +

`} + ${rules.equipment + ? html`
Equipment: ${Math.floor(s.equipment)} / 5,000

${s.holdings.filter( @@ -637,12 +652,25 @@ forged per second by completed smithies. Infantry and archers cost 30 per recruitment; knights cost 60.

-
-

+

` + : nothing} + ${rules.recruitment ? this.recruitCards(p, army, recruits) : nothing} + ${rules.exhaustion ? this.exhaustionNote(s) : nothing}`; + } + private recruitCards( + p: PlayerView, + army: ReturnType, + recruits: number, + ) { + const rules = modeRules(this.game); + const gear = (n: number) => (rules.equipment ? ` · ${n} equipment` : ""); + return html`

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."}

${( [ @@ -651,9 +679,9 @@ "Peasant levies", "60 crowns · 100 grain. Conscription reduces harvests and raises unrest.", ], - ["infantry", "Men-at-arms", "380 crowns · 100 grain · 30 equipment."], - ["archers", "Longbowmen", "380 crowns · 100 grain · 30 equipment."], - ["cavalry", "Knights", "810 crowns · 100 grain · 60 equipment."], + ["infantry", "Men-at-arms", `380 crowns · 100 grain${gear(30)}.`], + ["archers", "Longbowmen", `380 crowns · 100 grain${gear(30)}.`], + ["cavalry", "Knights", `810 crowns · 100 grain${gear(60)}.`], [ "mercenaries", "Mercenary company", @@ -688,8 +716,10 @@ Recruit ${recruits}
`, - )} -
+ )}`; + } + private exhaustionNote(s: NonNullable) { + return html`
Equipment ${fmt(s.equipment)}War exhaustion ${Math.round(s.exhaustion)}%
@@ -711,12 +741,18 @@ html`
${e.name()} ${p.isAlliedWith(e) ? "🤝 Allied" : ""}

- ⚔️ ${fmt(e.troops() + e.data.attackingTroops)} soldiers · ⌛ - ${Math.round(e.data.medieval?.exhaustion ?? 0)}% exhaustion + ⚔️ ${fmt(e.troops() + e.data.attackingTroops)} soldiers + ${modeRules(this.game).exhaustion + ? html`· ⌛ ${Math.round(e.data.medieval?.exhaustion ?? 0)}% + exhaustion` + : nothing}

- 🪙 ${fmt(e.gold())} crowns · 🌾 ${fmt(e.data.medieval?.food ?? 0)} - grain · 🗺️ ${fmt(e.numTilesOwned())} land + 🪙 ${fmt(e.gold())} crowns + ${modeRules(this.game).food + ? html`· 🌾 ${fmt(e.data.medieval?.food ?? 0)} grain` + : nothing} + · 🗺️ ${fmt(e.numTilesOwned())} land

`, )}`; @@ -748,7 +784,7 @@ export class MedievalMapLayer implements Layer { private icons: HTMLImageElement[] = Array.from({ length: 17 }, (_, i) => { const img = new Image(); - img.src = `/images/medieval/${i}.webp`; + img.src = `${BASE_PATH}/images/medieval/${i}.webp`; return img; }); constructor( diff --git a/src/client/MedievalMusic.ts b/src/client/MedievalMusic.ts --- a/src/client/MedievalMusic.ts +++ b/src/client/MedievalMusic.ts @@ -1,3 +1,4 @@ +import { BASE_PATH } from "../core/BasePath"; import { css, html, LitElement } from "lit"; import { customElement, state } from "lit/decorators.js"; import { UserSettings } from "../core/game/UserSettings"; @@ -9,13 +10,13 @@ return Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0.35; } const SOUNDTRACK = [ - { name: "March of the Shielded", url: "/music/march-of-the-shielded.mp3" }, - { name: "Silence of the Abbey", url: "/music/silence-of-the-abbey.mp3" }, - { name: "Medieval Dance Fusion", url: "/music/medieval-dance-fusion.mp3" }, - { name: "March of the Brave", url: "/music/march-of-the-brave.mp3" }, - { name: "Campestre Paz", url: "/music/campestre-paz.mp3" }, - { name: "Sunlit Horizons", url: "/music/sunlit-horizons.mp3" }, - { name: "Tavern Dance", url: "/music/tavern-dance.mp3" }, + { name: "March of the Shielded", url: BASE_PATH + "/music/march-of-the-shielded.mp3" }, + { name: "Silence of the Abbey", url: BASE_PATH + "/music/silence-of-the-abbey.mp3" }, + { name: "Medieval Dance Fusion", url: BASE_PATH + "/music/medieval-dance-fusion.mp3" }, + { name: "March of the Brave", url: BASE_PATH + "/music/march-of-the-brave.mp3" }, + { name: "Campestre Paz", url: BASE_PATH + "/music/campestre-paz.mp3" }, + { name: "Sunlit Horizons", url: BASE_PATH + "/music/sunlit-horizons.mp3" }, + { name: "Tavern Dance", url: BASE_PATH + "/music/tavern-dance.mp3" }, ]; @customElement("medieval-music") diff --git a/src/client/MedievalRealmSummary.ts b/src/client/MedievalRealmSummary.ts --- a/src/client/MedievalRealmSummary.ts +++ b/src/client/MedievalRealmSummary.ts @@ -1,3 +1,5 @@ +import { BASE_PATH } from "../core/BasePath"; +import { modeRules } from "../core/medieval/Modes"; import { html } from "lit"; import { GameView, PlayerView } from "../core/game/GameView"; import { BUILDINGS } from "../core/medieval/Medieval"; @@ -18,6 +20,7 @@ ? "Allied realm" : "Independent realm"; const active = s.holdings.filter((h) => h.ready <= game.ticks() && h.hp > 0); + const rules = modeRules(game); return html`
@@ -54,12 +59,14 @@
${( ["castle", "town", "farm", "smithy", "harbor", "watchtower"] as const - ).map( + ) + .filter((kind) => rules.buildings.includes(kind)) + .map( (kind) => html`${BUILDINGS[kind].name} ${active.filter((h) => h.kind === kind).length}Scholarship · ${researchSlots(s)}/5 slots -

- Projects are paid upfront and completed in order. Each branch price - doubles per queued level. -

+ const rules = modeRules(this.game); + const speed = scholarshipRate(s, this.game.ticks(), rules.productionMul); + return html`${rules.research + ? html`

Scholarship · ${researchSlots(s)}/5 slots

+

+ Projects are paid upfront and completed in order. Each branch + price doubles per queued level. +

` + : html`

Fleet

+

+ ${rules.label} mode has no research. Harbors can still reinforce + your transport hulls. +

`} ${(s.researchQueue ?? []).map( (q, i) => html`
${i + 2}. ${q.branch} · waiting
`, )} @@ -177,12 +185,18 @@ separately from scholarship.

-
+ ${rules.research ? this.researchCards(p, speed) : nothing}`; + } + private researchCards(p: PlayerView, speed: number) { + const s = p.data.medieval!; + return html`
Upgrade speed: ${speed.toFixed(3)} points/s

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)}%

@@ -270,7 +284,9 @@ }} >

-

📖 Research

+

+ ${modeRules(this.game).research ? "📖 Research" : "⚓ Fleet"} +

+ + ${extra} +
`; + } private open(tab: string) { document.dispatchEvent( new CustomEvent("open-medieval-council", { detail: { tab } }), @@ -236,7 +408,21 @@ this.updateLoot(s.conquestLoot); const fmt = (n: number | bigint) => Number(n).toLocaleString("en", { maximumFractionDigits: 0 }); - const metrics = [ + const rules = modeRules(this.game); + const hidden = new Set([ + ...(rules.food ? [] : ["Grain"]), + ...(rules.equipment ? [] : ["Equipment"]), + ...(rules.workers ? [] : ["Workers"]), + ...(rules.unrest ? [] : ["Unrest"]), + ...(rules.exhaustion ? [] : ["Exhaustion"]), + ]); + const spend = [ + "buildings", + ...(rules.recruitment ? ["troops"] : []), + ...(rules.research ? ["research"] : ["hull upgrades"]), + ...(rules.exhaustion ? ["feasts"] : []), + ]; + const allMetrics = [ [ "Royal host", fmt(p.troops() + p.data.attackingTroops), @@ -245,7 +431,7 @@ [ "Crowns", fmt(p.gold()), - `Income ${s.income.toFixed(1)}/s · upkeep ${s.upkeep ?? 0}/s. Crowns pay for buildings, troops, research and feasts.`, + `Income ${s.income.toFixed(1)}/s · upkeep ${s.upkeep ?? 0}/s. Crowns pay for ${spend.slice(0, -1).join(", ")} and ${spend[spend.length - 1]}.`, ], [ "Grain", @@ -260,12 +446,14 @@ [ "Workers", fmt(p.workers()), - "Civilian population supplies workers and tax income. Military recruitment reduces civilian workers. Castles and towns each add 12,000 population capacity.", + `Civilian population supplies workers and tax income. Military recruitment reduces civilian workers. Castles add 12,000 and towns add ${Math.round(15000 * rules.townMul).toLocaleString("en")} population capacity.`, ], [ "Land", fmt(p.numTilesOwned()), - "Owned map tiles. Land increases natural grain production, population capacity, feast cost and progress toward victory.", + rules.food + ? "Owned map tiles. Land increases natural grain production, population capacity, feast cost and progress toward victory." + : `Owned map tiles. Land increases population capacity and progress toward victory. Castles add 12,000 and towns add ${Math.round(15000 * rules.townMul).toLocaleString("en")} population capacity.`, ], [ "Unrest", @@ -278,16 +466,24 @@ "Losses and deployments exhaust the army and reduce damage. Recover gradually without deployed attacks, or host a feast to remove 10 percentage points.", ], ]; - const icons: Record = { - "Royal host": "⚔️", - Crowns: "🪙", - Grain: "🌾", - Equipment: "⚒️", - Workers: "👥", - Land: "🗺️", - Unrest: "🔥", - Exhaustion: "⌛", + const metrics = allMetrics.filter(([name]) => !hidden.has(name)); + const icon: Record = { + "Royal host": "host", + Crowns: "crowns", + Grain: "grain", + Equipment: "equipment", + Workers: "workers", + Land: "land", + Unrest: "unrest", + Exhaustion: "exhaustion", }; + // The plaque shows the essentials; the rest appear when they need attention. + const inline = metrics.filter( + ([name]) => + ["Royal host", "Crowns", "Grain", "Land"].includes(name) || + (name === "Unrest" && s.unrest >= 25) || + (name === "Exhaustion" && s.exhaustion >= 10), + ); const project = s.studying; const percent = project ? Math.min( @@ -300,117 +496,157 @@ ), ) : 0; - return html`
-
- ${metrics.map( - ([name, value, tip]) => - html`
- ${name}${value}${tip} - ${name === "Crowns" && this.lootGain > 0n - ? html`+${this.lootGain.toLocaleString()}` - : html``} -
`, + const singleplayer = + this.game.config().gameConfig().gameType === GameType.Singleplayer || + this.game.config().isReplay(); + const realmPop = html``; + const homePop = html``; + return html`
+
+ ${inline.map( + ([name, value]) => + html`${name}${value}`, + )} + ${this.lootGain > 0n + ? html`+${this.lootGain.toLocaleString()} crowns` + : nothing} + +
+
+ ${this.tile("army", "Army", () => this.open("Army"))} + ${this.tile("diplomacy", "Diplomacy", () => this.open("Diplomacy"))} + ${this.tile( + rules.research ? "research" : "fleet", + rules.research ? "Research" : "Fleet", + () => this.open("Knowledge"), + rules.research && project + ? html`` + : html``, + )} + ${this.tile( + "realm", + "Realm", + () => this.toggle("realm"), + this.menu === "realm" ? realmPop : html``, + this.menu === "realm", + )} + ${this.tile( + "home", + "Home", + () => this.toggle("home"), + this.menu === "home" ? homePop : html``, + this.menu === "home", )}
-
- - - - ${this.game.config().gameConfig().gameType === GameType.Singleplayer || - this.game.config().isReplay() - ? html` ` - : html``} - - -
this.open("Knowledge")} - @keydown=${(e: KeyboardEvent) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - this.open("Knowledge"); - } - }} - > -
- 📖 - ${project - ? researchName(project.branch, s.research[project.branch]) - : "No research active"}${percent}% -
-
- -
-
-
`; } } diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -1,3 +1,4 @@ +import { BASE_PATH } from "../core/BasePath"; import { LitElement, html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { translateText } from "../client/Utils"; @@ -56,7 +57,7 @@ async fetchLobbies(): Promise { try { - const response = await fetch(`/api/public_lobbies`); + const response = await fetch(`${BASE_PATH}/api/public_lobbies`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const data = await response.json(); diff --git a/src/client/RankingsModal.ts b/src/client/RankingsModal.ts --- a/src/client/RankingsModal.ts +++ b/src/client/RankingsModal.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 { translateText } from "./Utils"; @@ -78,7 +79,7 @@ try { if (this.activeTab === "players") { - const response = await fetch("/api/rankings?limit=100"); + const response = await fetch(BASE_PATH + "/api/rankings?limit=100"); if (!response.ok) { throw new Error(`Failed to fetch rankings: ${response.status}`); } @@ -86,7 +87,7 @@ this.playerLeaderboard = data.leaderboard; this.totalPlayers = data.totalPlayers; } else if (this.activeTab === "clans") { - const response = await fetch("/api/rankings/clans?limit=100"); + const response = await fetch(BASE_PATH + "/api/rankings/clans?limit=100"); if (!response.ok) { throw new Error(`Failed to fetch clan rankings: ${response.status}`); } @@ -94,7 +95,7 @@ this.clanLeaderboard = data.leaderboard; this.totalClans = data.totalClans; } else { - const response = await fetch("/api/rankings/hall-of-fame"); + const response = await fetch(BASE_PATH + "/api/rankings/hall-of-fame"); if (!response.ok) { throw new Error(`Failed to fetch hall of fame: ${response.status}`); } diff --git a/src/client/RealmModePicker.ts b/src/client/RealmModePicker.ts new file mode 100644 --- /dev/null +++ b/src/client/RealmModePicker.ts @@ -0,0 +1,102 @@ +// veryimportantmeeting.com edition: realm mode picker on the home screen (2026-09-28). +import { + DEFAULT_REALM_MODE, + isRealmMode, + MODE_RULES, + REALM_MODES, + RealmMode, +} from "../core/medieval/Modes"; + +const KEY = "cc-realm-mode"; +const CAPTIONS: Record = { + simple: "Build and conquer", + normal: "Grain and armies", + advanced: "The full kingdom", +}; + +export function getStoredRealmMode(): RealmMode { + try { + const value = localStorage.getItem(KEY); + if (isRealmMode(value)) return value; + } catch { + // Storage can be blocked; fall back to the default. + } + return DEFAULT_REALM_MODE; +} + +export function setStoredRealmMode(mode: RealmMode) { + try { + localStorage.setItem(KEY, mode); + } catch { + // Not fatal: the choice still applies to this visit. + } + memory = mode; + document.dispatchEvent(new CustomEvent("realm-mode-change", { detail: mode })); +} + +let memory: RealmMode | null = null; +export function currentRealmMode(): RealmMode { + return memory ?? getStoredRealmMode(); +} + +export function mountRealmModePicker() { + const quick = document.getElementById("quick-battle"); + if (!quick || document.getElementById("realm-mode")) return; + const selected = currentRealmMode(); + const field = document.createElement("fieldset"); + field.id = "realm-mode"; + field.className = "realm-mode"; + field.setAttribute("aria-describedby", "realm-mode-note"); + const legend = document.createElement("legend"); + legend.textContent = "Realm mode"; + const options = document.createElement("div"); + options.className = "realm-mode__options"; + const note = document.createElement("p"); + note.id = "realm-mode-note"; + note.className = "realm-mode__note"; + note.setAttribute("aria-live", "polite"); + for (const mode of REALM_MODES) { + const label = document.createElement("label"); + const input = document.createElement("input"); + input.type = "radio"; + input.name = "realm-mode"; + input.value = mode; + input.checked = mode === selected; + input.addEventListener("change", () => { + if (!input.checked) return; + setStoredRealmMode(mode); + note.textContent = MODE_RULES[mode].summary; + }); + const face = document.createElement("span"); + const name = document.createElement("b"); + name.textContent = MODE_RULES[mode].label; + const caption = document.createElement("small"); + caption.textContent = CAPTIONS[mode]; + face.append(name, caption); + label.append(input, face); + options.append(label); + } + note.textContent = MODE_RULES[selected].summary; + document.addEventListener("realm-mode-change", (e) => { + const mode = (e as CustomEvent).detail as RealmMode; + for (const input of options.querySelectorAll("input")) + input.checked = input.value === mode; + note.textContent = MODE_RULES[mode].summary; + }); + field.append(legend, options, note); + quick.insertAdjacentElement("afterend", field); +} + +// veryimportantmeeting.com edition: link back to the arcade home page (2026-09-28). +export const ARCADE_HOME = "https://veryimportantmeeting.com/"; +export function mountHomeLink() { + if (document.getElementById("vim-home-link")) return; + const link = document.createElement("a"); + link.id = "vim-home-link"; + link.className = "vim-home-link"; + link.href = ARCADE_HOME; + link.textContent = "Home"; + link.title = "Back to the veryimportantmeeting.com homepage"; + link.setAttribute("aria-label", "Back to the veryimportantmeeting.com homepage"); + document.body.append(link); +} diff --git a/src/client/RoomSocket.ts b/src/client/RoomSocket.ts --- a/src/client/RoomSocket.ts +++ b/src/client/RoomSocket.ts @@ -1,3 +1,4 @@ +import { BASE_PATH } from "../core/BasePath"; // HTTP lockstep transport for the independent hosted lobby service. // Game simulation and turn validation remain in the original engine. export class RoomSocket { @@ -20,7 +21,7 @@ queueMicrotask(() => { this.readyState = 1; this.onopen?.(); }); } private async request(path: string, body?: unknown) { - const response = await fetch(`/api/rooms/${this.gameID}/${path}`, { method: body === undefined ? "GET" : "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.auth.token}` }, body: body === undefined ? undefined : JSON.stringify(body) }); + const response = await fetch(`${BASE_PATH}/api/rooms/${this.gameID}/${path}`, { method: body === undefined ? "GET" : "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.auth.token}` }, body: body === undefined ? undefined : JSON.stringify(body) }); const result = await response.json(); if (!response.ok) throw new Error(result.error || "Connection interrupted"); return result; diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -14,8 +14,11 @@ } from "../core/game/Game"; import { MEDIEVAL_MAPS, + MEDIEVAL_MAP_BOTS, MEDIEVAL_MAP_LABELS, } from "../core/medieval/MedievalMaps"; +import { MODE_RULES, REALM_MODES, RealmMode } from "../core/medieval/Modes"; +import { currentRealmMode, setStoredRealmMode } from "./RealmModePicker"; import type { TeamCountConfig } from "../core/Schemas"; import { GoldMultiplierValues, @@ -50,6 +53,7 @@ }; @state() private selectedMap: GameMapType = GameMapType.Europe; @state() private selectedDifficulty: Difficulty = Difficulty.Medium; + @state() private realmMode: RealmMode = currentRealmMode(); @state() private disableNPCs: boolean = false; @state() private bots: number = 80; @state() private infiniteGold: boolean = false; @@ -503,6 +507,36 @@
▼
+
+ + +
+
Realm mode
+ +

+ ${MODE_RULES[this.realmMode].summary} +

@@ -792,6 +826,7 @@ } public open() { + this.realmMode = currentRealmMode(); this.modalEl?.open(); this.useRandomMap = false; this.startingGold = 0; @@ -809,6 +844,8 @@ private handleMapSelection(value: GameMapType) { this.selectedMap = value; this.useRandomMap = false; + // veryimportantmeeting.com edition: small maps get fewer wandering tribes. + this.bots = MEDIEVAL_MAP_BOTS[value] ?? this.bots; } private handleDifficultySelection(value: Difficulty) { @@ -892,6 +929,7 @@ // If random map is selected, choose a random map now if (this.useRandomMap) { this.selectedMap = this.getRandomMap(); + this.bots = MEDIEVAL_MAP_BOTS[this.selectedMap] ?? this.bots; } console.log( @@ -930,6 +968,7 @@ ], config: { medieval: true, + medievalMode: this.realmMode, gameMap: this.selectedMap, gameType: GameType.Singleplayer, gameMode: this.gameMode, diff --git a/src/client/command.css b/src/client/command.css --- a/src/client/command.css +++ b/src/client/command.css @@ -502,3 +502,640 @@ padding: 0 5px !important; } .medieval-game options-menu .settings-grid > div { grid-column: 1 / -1; } + +/* veryimportantmeeting.com edition: realm mode picker (2026-09-28) */ +.realm-mode { + margin: 12px 0 0; + padding: 10px 12px 12px; + border: 1px solid #bb905865; + border-radius: 8px; + background: linear-gradient(180deg, #2b2218f2, #1f1912f2); + color: #f5dfae; + min-inline-size: 0; +} +.realm-mode legend { + padding: 0 6px; + font: + 14px Georgia, + serif; + letter-spacing: 0.5px; + color: #e3c17f; +} +.realm-mode__options { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 6px; +} +.realm-mode label { + position: relative; + cursor: pointer; +} +.realm-mode input { + position: absolute; + opacity: 0; + inset: 0; + margin: 0; + cursor: pointer; +} +.realm-mode label > span { + display: flex; + flex-direction: column; + gap: 2px; + height: 100%; + box-sizing: border-box; + padding: 8px 10px 9px; + border: 1px solid #80633f; + border-radius: 6px; + background: #352b1f; + color: #ead9b9; + transition: + background-color 180ms cubic-bezier(0.16, 1, 0.3, 1), + border-color 180ms cubic-bezier(0.16, 1, 0.3, 1), + box-shadow 180ms cubic-bezier(0.16, 1, 0.3, 1); +} +.realm-mode label > span b { + font: + 600 16px Georgia, + serif; + color: inherit; +} +.realm-mode label > span small { + font: + 12px/1.3 system-ui, + sans-serif; + color: #cbb994; +} +.realm-mode label:hover > span { + border-color: #b08f5a; + background: #3f3223; +} +.realm-mode input:checked + span { + border-color: #e0b96e; + background: linear-gradient(180deg, #6b4833, #563a29); + color: #fbe9c3; + box-shadow: + inset 0 1px 0 #f3d39a33, + 0 2px 8px #0008; +} +.realm-mode input:checked + span small { + color: #ecd7ae; +} +.realm-mode input:focus-visible + span { + outline: 2px solid #efd49a; + outline-offset: 2px; +} +.realm-mode__note { + margin: 9px 2px 0; + min-height: 2.8em; + font: + 12.5px/1.4 system-ui, + sans-serif; + color: #d2c4ab; + text-wrap: pretty; +} +@media (max-width: 420px) { + .realm-mode label > span { + padding: 7px 8px 8px; + } + .realm-mode label > span b { + font-size: 15px; + } + .realm-mode label > span small { + font-size: 11px; + } +} +@media (prefers-reduced-motion: reduce) { + .realm-mode label > span { + transition: none; + } +} + +/* veryimportantmeeting.com edition: link back to the arcade (2026-09-28) */ +.vim-home-link { + position: fixed; + top: calc(44px + env(safe-area-inset-top, 0px)); + left: 12px; + z-index: 60; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 12px; + border: 1px solid #ac8d5688; + border-radius: 6px; + background: #231f19d9; + color: #e5c184; + font: + 15px Georgia, + serif; + text-decoration: none; +} +.vim-home-link::before { + content: "\2190"; +} +.vim-home-link:hover { + border-color: #e0b96e; + color: #f5dfae; +} +.vim-home-link:focus-visible { + outline: 2px solid #efd49a; + outline-offset: 2px; +} +.medieval-game .vim-home-link { + display: none; +} +@media (max-width: 600px) { + .vim-home-link { + position: absolute; + font-size: 13px; + padding: 5px 9px; + } +} + +/* ===== veryimportantmeeting.com edition: Oak Workshop B2 (2026-09-28) ===== */ +@font-face { + font-family: "Pixelify Sans"; + src: url("../../resources/fonts/ui/pixelify-sans-latin-400-normal.woff2") format("woff2"); + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: "Pixelify Sans"; + src: url("../../resources/fonts/ui/pixelify-sans-latin-700-normal.woff2") format("woff2"); + font-weight: 700; + font-display: swap; +} +@font-face { + font-family: "Jersey 10"; + src: url("../../resources/fonts/ui/jersey-10-latin-400-normal.woff2") format("woff2"); + font-weight: 400; + font-display: swap; +} +:root { + --oak-ink: #2b1a0d; + --oak-cream: #fff4da; + --oak-edge: #b8742f; + --oak-night: #231a10; + --oak-pix: "Pixelify Sans", "Courier New", monospace; +} +.oak-plaque { + border: 12px solid transparent; + border-image: url("../../resources/images/ui/oak-panel.png") 14 fill / 21px stretch; + image-rendering: pixelated; + color: var(--oak-ink); + font-family: var(--oak-pix); +} +.oak-tile { + position: relative; + width: 52px; + height: 52px; + border: 0; + padding: 0; + background: url("../../resources/images/ui/oak-tile.png") center / 100% 100%; + image-rendering: pixelated; + display: grid; + place-items: center; + cursor: pointer; + flex: none; +} +.oak-tile:hover:not(:disabled) { + filter: brightness(1.1); +} +.oak-tile:active:not(:disabled) { + background-image: url("../../resources/images/ui/oak-tile-down.png"); +} +.oak-tile:disabled { + filter: grayscale(0.7) brightness(0.8); + cursor: default; +} +.oak-tile:focus-visible { + outline: 2px solid var(--oak-cream); + outline-offset: 2px; +} +.oak-tile > img { + width: 30px; + height: 30px; + image-rendering: pixelated; +} +.oak-tile.oak-build > img { + width: 36px; + height: 36px; + object-fit: contain; + image-rendering: auto; + margin-top: -8px; +} +.oak-build small { + position: absolute; + left: 0; + right: 0; + bottom: 5px; + text-align: center; + font: 700 11px/1 var(--oak-pix); + color: var(--oak-cream); + text-shadow: + 1px 1px 0 var(--oak-ink), + -1px 0 0 var(--oak-ink), + 0 -1px 0 var(--oak-ink); +} +.oak-bottom { + zoom: var(--ui-scale, 0.9); + position: relative; + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: end; + gap: 8px; + padding: 0 10px 10px; + pointer-events: none; +} +.oak-bottom > * { + pointer-events: auto; +} +.oak-host { + justify-self: start; + position: relative; + display: grid; + gap: 4px; + width: 230px; + padding: 0 6px; + outline: none; +} +.oak-host-line { + display: flex; + align-items: center; + gap: 6px; + font-size: 15px; +} +.oak-host-line b { + font-size: 18px; + font-variant-numeric: tabular-nums; +} +.oak-host-line img { + width: 24px; + height: 24px; + image-rendering: pixelated; +} +.oak-slider { + display: grid; + gap: 2px; + font: 700 12px var(--oak-pix); +} +.oak-slider input { + width: 100%; + margin: 0; +} +.oak-tip { + display: none; + position: absolute; + bottom: calc(100% + 10px); + left: -12px; + width: 280px; + padding: 10px 12px; + background: #231a10f2; + border: 2px solid var(--oak-edge); + border-radius: 6px; + box-shadow: 0 4px 14px #0009; + color: var(--oak-cream); + font: 13px/1.45 system-ui, sans-serif; + z-index: 5; +} +.oak-tip p { + margin: 4px 0 0; + color: #e9d6ae; +} +.oak-host:hover .oak-tip, +.oak-host:focus .oak-tip { + display: block; +} +.oak-host:focus-within:not(:focus) .oak-tip { + display: none; +} +.oak-tray-wrap { + display: grid; + justify-items: center; + gap: 6px; +} +.oak-tray { + display: flex; + gap: 5px; + padding: 0 2px; +} +.oak-notice { + max-width: 480px; + padding: 5px 12px; + border-radius: 5px; + background: #231a10e6; + border: 1px solid #b8742f99; + color: var(--oak-cream); + font: 13px system-ui, sans-serif; + text-align: center; + pointer-events: none; +} +.medieval-game control-panel input.medieval-host-range { + height: 8px; + accent-color: #7b3f12; +} +/* Floating plaques let the map show between them. */ +.medieval-game .bottom-0:has(control-panel) { + pointer-events: none !important; +} +.medieval-game .ui-scale-surface:has(> control-panel) { + pointer-events: none !important; +} +.medieval-game control-panel { + pointer-events: none !important; +} +@media (min-width: 701px) { + .medieval-game #game-status-dock, + .medieval-game options-menu { + top: 8px !important; + } +} +/* Events panel: low on wide screens, lifted above the plaques when space is short. */ +@media (min-width: 1200px) { + .medieval-game #bottom-hud { + bottom: 12px !important; + } +} +@media (max-width: 760px) { + .medieval-game #bottom-hud { + bottom: 200px !important; + } + .oak-bottom { + grid-template-columns: 1fr; + justify-items: start; + padding: 0 6px 6px; + gap: 6px; + } + .oak-tray-wrap { + order: -1; + justify-self: stretch; + justify-items: start; + min-width: 0; + } + .oak-tray { + max-width: 100%; + overflow-x: auto; + overscroll-behavior-x: contain; + scrollbar-width: none; + } + .oak-tile { + width: 46px; + height: 46px; + } + .oak-host { + width: min(260px, 100%); + } + .oak-notice { + justify-self: start; + } +} +.oak-tile:disabled { + filter: grayscale(0.45) brightness(0.92); + opacity: 0.8; +} + +/* ----- Oak Workshop home screen ----- */ +main .container.submarine-panel { + position: relative; + background: none !important; + border: 20px solid transparent !important; + border-image: url("../../resources/images/ui/oak-panel.png") 14 fill / 30px round !important; + image-rendering: pixelated; + box-shadow: 0 18px 40px #000a !important; + padding: 58px 18px 18px !important; + margin-top: 46px; + color: var(--oak-ink); +} +main .command-brand { + gap: 2px; +} +main .command-brand .command-mark { + position: absolute; + top: -70px; + left: 50%; + transform: translateX(-50%); + width: 92px; + height: 92px; + padding: 0; + border: 0; + clip-path: none; + font-size: 0; + background: url("../../resources/images/ui/oak-round.png") center / 100% 100% no-repeat; + image-rendering: pixelated; + display: grid; + place-items: center; +} +main .command-brand .command-mark::after { + content: ""; + width: 62px; + height: 62px; + background: url("../../resources/images/medieval/0.webp") center / contain no-repeat; +} +main .command-brand h1 { + font-family: "Jersey 10", var(--oak-pix); + font-weight: 400; + font-size: 52px; + line-height: 0.95; + letter-spacing: 2px; + color: var(--oak-cream); + text-shadow: + 3px 3px 0 #3b1f0b, + -1px 0 0 #3b1f0b, + 0 -1px 0 #3b1f0b; +} +main .command-brand h1 i { + font-style: normal; + font-size: 36px; +} +main .command-brand p { + font-family: var(--oak-pix); + font-size: 12px; + color: var(--oak-ink); + letter-spacing: 1.5px; +} +main .command-brand > span { + font-family: var(--oak-pix); + color: #5a3414; + border-color: #5a341466; +} +main .command-quick { + min-height: 0; + background: none !important; + border: 14px solid transparent; + border-image: url("../../resources/images/ui/oak-panel-sm.png") 14 fill / 21px stretch; + image-rendering: pixelated; + color: var(--oak-ink); + padding: 8px 14px; + text-align: center; + filter: drop-shadow(0 3px 0 #3b1f0b66); +} +main .command-quick > span { + font: 11px var(--oak-pix); + letter-spacing: 1.5px; + color: #5a3414; +} +main .command-quick strong { + font: 700 26px/1.1 var(--oak-pix); + color: var(--oak-cream); + text-shadow: + 2px 2px 0 #3b1f0b, + -1px 0 0 #3b1f0b, + 0 -1px 0 #3b1f0b; + margin: 4px 0; +} +main .command-quick small { + font: 12px var(--oak-pix); + color: var(--oak-ink); +} +main .command-quick b { + color: var(--oak-cream); + text-shadow: 2px 2px 0 #3b1f0b; + top: 50%; + transform: translateY(-50%); +} +main .command-quick:hover { + filter: brightness(1.08) drop-shadow(0 3px 0 #3b1f0b66); +} +main .realm-mode { + border: 0; + background: #3b1f0b33; + border-radius: 6px; + color: var(--oak-ink); + padding: 4px 8px 8px; +} +main .realm-mode legend { + font: 700 13px var(--oak-pix); + color: var(--oak-ink); + background: none; + padding: 2px 2px 4px; +} +main .realm-mode label > span { + border: 10px solid transparent; + border-image: url("../../resources/images/ui/oak-panel-sm.png") 14 fill / 15px stretch; + image-rendering: pixelated; + background: none; + border-radius: 0; + padding: 2px 4px; + color: var(--oak-ink); + transition: filter 150ms ease-out; +} +main .realm-mode label > span b { + font: 700 15px var(--oak-pix); +} +main .realm-mode label > span small { + font: 11px/1.25 var(--oak-pix); + color: #4a2a10; +} +main .realm-mode label:hover > span { + background: none; + border-color: transparent; + filter: brightness(1.08); +} +main .realm-mode input:checked + span { + background: none; + border-color: transparent; + box-shadow: none; + color: var(--oak-cream); + filter: brightness(1.12) saturate(1.1) drop-shadow(0 0 0 #fff4da) + drop-shadow(0 0 2px #fff4da); + text-shadow: 1px 1px 0 #3b1f0b; +} +main .realm-mode input:checked + span small { + color: #fff4da; +} +main .realm-mode input:focus-visible + span { + outline: 2px solid var(--oak-ink); + outline-offset: 1px; +} +main .realm-mode__note { + color: var(--oak-ink); + font: 12.5px/1.4 system-ui, sans-serif; + margin-top: 6px; +} +main o-button .c-button { + background: none !important; + border: 12px solid transparent !important; + border-image: url("../../resources/images/ui/oak-panel-sm.png") 14 fill / 18px stretch !important; + image-rendering: pixelated; + color: var(--oak-cream) !important; + font: 700 17px var(--oak-pix) !important; + text-shadow: + 2px 2px 0 #3b1f0b, + -1px 0 0 #3b1f0b; + box-shadow: none !important; + padding: 2px 10px !important; + min-height: 0 !important; +} +main o-button .c-button:hover { + filter: brightness(1.08); +} +main lang-selector button { + background: #3b1f0b33 !important; + color: var(--oak-ink) !important; + border-color: #5a341466 !important; + font-family: var(--oak-pix) !important; +} +/* Home sign: wooden arrow pointing back to the arcade. */ +.vim-home-link { + top: calc(40px + env(safe-area-inset-top, 0px)); + width: 132px; + height: 66px; + padding: 0 0 0 26px; + border: 0; + border-radius: 0; + background: url("../../resources/images/ui/oak-arrow-left.png") center / 100% 100% no-repeat; + image-rendering: pixelated; + justify-content: center; + color: var(--oak-cream); + font: 700 18px var(--oak-pix); + text-shadow: + 2px 2px 0 #3b1f0b, + -1px 0 0 #3b1f0b; + filter: drop-shadow(0 4px 6px #000a); +} +.vim-home-link::before { + content: none; +} +.vim-home-link:hover { + color: var(--oak-cream); + filter: brightness(1.1) drop-shadow(0 4px 6px #000a); +} +@media (max-width: 600px) { + .vim-home-link { + position: absolute; + width: 104px; + height: 52px; + font-size: 15px; + padding-left: 20px; + } + main .container.submarine-panel { + margin-top: 100px; + } + main .command-brand h1 { + font-size: 42px; + } +} +@media (max-width: 600px) { + main .command-quick { + padding-right: 34px; + } +} + +/* Larger building pictures with persistent names and keyboard shortcuts. */ +.oak-tray-wrap { min-width: 0; max-width: min(100%, calc(100vw / var(--ui-scale, 1) - 290px)); } +.oak-tray { max-width: 100%; overflow-x: auto; overscroll-behavior-x: contain; padding: 2px 2px 5px; } +.oak-tile { width: 82px; height: 104px; grid-template-rows: 62px 22px; align-content: start; padding-top: 4px; } +.oak-tile.oak-build > img { width: 56px; height: 56px; margin: 0; } +.oak-building-name { color: var(--oak-cream); font: 700 11px/1.1 system-ui, sans-serif; text-align: center; text-shadow: 1px 1px #27180e; padding: 0 3px; } +.oak-building-key { position: absolute; top: 4px; right: 5px; padding: 2px 3px; background: #27180ee6; color: #fff2c8; border: 1px solid #b68c55; border-radius: 3px; font: 700 10px/1 monospace; } +.oak-build small { bottom: 7px; font-size: 12px; } +@media (max-width: 1000px) { .oak-bottom { grid-template-columns: 1fr; justify-items: start; } .oak-tray-wrap { order: -1; max-width: 100%; justify-self: stretch; } } + +/* Larger building pictures with persistent names and keyboard shortcuts. */ +.oak-tray-wrap { min-width: 0; max-width: min(100%, calc(100vw / var(--ui-scale, 1) - 290px)); } +.oak-tray { max-width: 100%; overflow-x: auto; overscroll-behavior-x: contain; padding: 2px 2px 5px; } +.oak-tile { width: 82px; height: 104px; grid-template-rows: 62px 22px; align-content: start; padding-top: 4px; } +.oak-tile.oak-build > img { width: 56px; height: 56px; margin: 0; } +.oak-building-name { color: var(--oak-cream); font: 700 11px/1.1 system-ui, sans-serif; text-align: center; text-shadow: 1px 1px #27180e; padding: 0 3px; } +.oak-building-key { position: absolute; top: 4px; right: 5px; padding: 2px 3px; background: #27180ee6; color: #fff2c8; border: 1px solid #b68c55; border-radius: 3px; font: 700 10px/1 monospace; } +.oak-build small { bottom: 7px; font-size: 12px; } +@media (max-width: 1000px) { .oak-bottom { grid-template-columns: 1fr; justify-items: start; } .oak-tray-wrap { order: -1; max-width: 100%; justify-self: stretch; } } diff --git a/src/client/components/Maps.ts b/src/client/components/Maps.ts --- a/src/client/components/Maps.ts +++ b/src/client/components/Maps.ts @@ -31,6 +31,13 @@ Halkidiki: "Halkidiki", StraitOfGibraltar: "Strait of Gibraltar", Italia: "Italia", + Scotia: "Scotia", + Cymru: "Cymru", + Hibernia: "Hibernia", + NarrowSea: "Narrow Sea", + HolyRomanEmpire: "Holy Roman Empire", + Scandinavia: "Scandinavia", + Aegean: "Aegean", Nukewars1024: "Nukewars 1024", NukeWars2: "NukeWars 2", NukeWars2000: "NukeWars 2000", diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts --- a/src/client/graphics/layers/ControlPanel.ts +++ b/src/client/graphics/layers/ControlPanel.ts @@ -1,3 +1,6 @@ +import { BASE_PATH } from "../../../core/BasePath"; +import type { HoldingKind } from "../../../core/medieval/Medieval"; +import { buildingDescription, modeRules } from "../../../core/medieval/Modes"; import { html, LitElement } from "lit"; import { customElement, state } from "lit/decorators.js"; import { translateText } from "../../../client/Utils"; @@ -7,6 +10,7 @@ import { armyStats, BUILDINGS, + BUILD_HOTKEYS, RESEARCH_INCREMENT, SOLDIER_POWER, } from "../../../core/medieval/Medieval"; @@ -338,100 +342,25 @@ bonus: 0, n: Math.max(0, available - classes.reduce((n, c) => n + c.n, 0)), }); - return html`
-
- ${Object.entries(BUILDINGS) - .filter( - ([k]) => !["road", "bridge", "trebuchet", "catapult"].includes(k), - ) - .map( - ([kind, b]) => - html``, - )} - -
-
- ${s.notices[0] ?? "Select a building, then click your land."} -
-
- Royal host · ${available.toLocaleString()} ready -
-
- ${classes.map( - (c) => - html`${c.name} ${c.n.toLocaleString()} ${c.bonus > 0 - ? `+${c.bonus.toFixed(2)}% damage` - : ""}`, - )} -
-
- Army damage - ${( - (1 + armyStats(s, p.troops(), p.data.attackingTroops).bonus) * - (1 + s.research.warfare * RESEARCH_INCREMENT) * - 100 - ).toFixed(2)}% - · Normal army 100% · Attack power - ${Math.round( - armyStats(s, p.troops(), p.data.attackingTroops).power * - (1 + s.research.warfare * RESEARCH_INCREMENT), - ).toLocaleString()} - · ${Math.floor(p.data.attackingTroops).toLocaleString()} deployed · - Research - +${(s.research.warfare * RESEARCH_INCREMENT * 100).toFixed(2)}% -
-
-