Improve cacheability with content-hashed public assets and a cacheable app shell (#3494)

## Description:

This reworks asset delivery and cacheability across the app and moves
non-bundled public resources onto immutable, content-hashed URLs.

Vite bundle outputs continue to live under `/assets/**` and remain
content-hashed by Vite. Public resources that were previously fetched
from stable paths in `resources/` now go through a custom hashed
namespace under `/_assets/**`, backed by a generated asset manifest that
is available to the server, browser, and worker runtime.

In parallel, the root app shell is now cacheable shared HTML instead of
request-time `no-store` HTML. Dynamic and live routes remain explicitly
uncached.

## Why
- Improve browser and Cloudflare cacheability for static assets.
- Remove query-string and release-version cache busting for
runtime-fetched assets.
- Allow unchanged public assets to keep the same URL across releases.
- Reduce avoidable work on `/` by serving a shared app shell instead of
rendering HTML on every request.
- Make cache behavior explicit instead of relying on mixed framework
defaults and file-extension heuristics.

## What Changed

### 1. Content-hashed public asset pipeline
- Added a build-time public asset manifest and hashing pipeline for
non-Vite resources.
- Production now emits hashed public assets under `/_assets/**`.
- Added runtime manifest loading for Node so server-rendered paths
resolve against built hashed files instead of rebuilding from source at
runtime.
- Emitted the runtime asset manifest as an ESM module for server
consumption.

Result:
- `/assets/**` = Vite-managed hashed bundle outputs
- `/_assets/**` = custom content-hashed public resources

### 2. Runtime asset URL migration
- Added a shared `assetUrl(...)` resolution path.
- Migrated runtime references away from query-string versioning and
stable source paths.
- Updated browser, worker, and server-side rendering paths to resolve
through the asset manifest.
- Moved map manifests, map binaries, thumbnails, sprites, sounds, fonts,
flags, icons, screenshots, and other runtime-fetched resources onto
hashed URLs.

### 3. Map and preview fixes
- Fixed directory and per-file map asset resolution so map manifest and
binary fetches resolve to the correct hashed URLs.
- Updated preview metadata and map thumbnail paths to use the hashed
asset namespace.
- Fixed runtime manifest loading in prod after deployment.

### 4. Explicit cache policies
- Added explicit immutable cache headers for:
  - `/assets/**`
  - `/_assets/**`
  - worker-prefixed equivalents under `/wN/...`
- Added explicit `no-store` headers for live and dynamic APIs.
- Removed the old `/api/env` bootstrap request and baked `gameEnv` into
the HTML bootstrap instead.

### 5. Cacheable root app shell
- Refactored the root HTML path to serve a shared app shell with:
- `Cache-Control: public, max-age=0, s-maxage=300,
stale-while-revalidate=86400`
- `/` and the SPA fallback now serve shared cacheable HTML instead of
request-time `no-store` rendering.
- `/game/:id` remains dynamic and `no-store`, but now reuses the shared
shell before injecting preview tags.

### 6. Matchmaking instance handling
- Because the app shell is now cacheable, `INSTANCE_ID` was removed from
shared HTML.
- Added `/api/instance` as a temporary `no-store` runtime lookup used
only by matchmaking.
- This preserves correctness with the current random-per-boot
`INSTANCE_ID` model while keeping `/` cacheable, but it is not the
intended long-term design.

## Behavior Changes

### Asset URL contract
Production URLs for non-Vite public resources now change from stable
paths such as:
- `/maps/...`
- `/images/...`
- `/manifest.json`

to content-hashed paths under:
- `/_assets/...`

Examples:
- `/_assets/maps/<map>/manifest.<hash>.json`
- `/_assets/images/Favicon.<hash>.svg`

### Bootstrap/config
- `/api/env` is removed.
- `gameEnv` is now bootstrapped from HTML.

### HTML caching
- `/` and the SPA fallback are now cacheable shared HTML.
- `/game/:id` remains dynamic.

## Cache Matrix After This Branch
- `/_assets/**`: `public, max-age=31536000, immutable`
- `/assets/**`: `public, max-age=31536000, immutable`
- live `/api/**`: explicit `no-store`
- `/api/health`: explicit `no-store`
- `/api/instance`: explicit `no-store`
- `/game/:id`: explicit `no-store`
- `/` and SPA fallback: `public, max-age=0, s-maxage=300,
stale-while-revalidate=86400`

## Notes / Tradeoffs
- `/api/instance` is a temporary compromise. It exists because
`INSTANCE_ID` is currently random per boot, which is not safe to embed
into cacheable shared HTML.
- The current matchmaking flow still asks the client to provide
`instance_id` during `matchmaking/join`. That is functional, but it is
the wrong ownership boundary: instance selection should be handled by
the matchmaking service, not by the browser.
- The cleaner end-state would be:
- make `matchmaking/join` stop requiring `instance_id` from the client,
and let the matchmaking service select a healthy instance from worker
check-ins
- This branch makes the origin behavior edge-cache-friendly, but
Cloudflare still needs matching cache rules if HTML itself should be
cached at the edge.

## Validation
Verified during development with:
- `npx tsc --noEmit`
- `node node_modules\\vite\\bin\\vite.js build`
- `node node_modules\\vitest\\vitest.mjs run
tests/server/RenderHtml.test.ts tests/server/NoStoreHeaders.test.ts
tests/server/StaticAssetCache.test.ts
tests/core/configuration/ConfigLoader.test.ts`

Additional targeted tests added:
- `tests/AssetUrls.test.ts`
- `tests/core/game/FetchGameMapLoader.test.ts`
- `tests/core/configuration/ConfigLoader.test.ts`
- `tests/server/NoStoreHeaders.test.ts`
- `tests/server/StaticAssetCache.test.ts`
- `tests/server/RenderHtml.test.ts`

## Known Existing Warnings
The production build still reports pre-existing warnings that are not
addressed by this branch:
- inconsistent JSON import attributes for `resources/countries.json`
- inconsistent JSON import attributes for `resources/QuickChat.json`
- large chunk warnings from Vite

## Rollout Notes
- Cache rules should treat `/_assets/**` and `/assets/**` as immutable.
- Cloudflare will still classify HTML as dynamic after deploy unless
matching edge cache rules are configured for it.

## Follow-ups
- Remove `/api/instance` by changing `matchmaking/join` so the server
selects the target instance, or by making `INSTANCE_ID` deploy-stable if
the current contract must remain.


## Please complete the following:

- [ ] I have added screenshots for all UI updates
- [ ] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [ ] I have added relevant tests to the test directory
- [ ] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

DISCORD_USERNAME
This commit is contained in:
scamiv
2026-03-23 19:36:52 +01:00
committed by GitHub
parent e3a14671ab
commit 05e2bc9f0a
65 changed files with 1214 additions and 331 deletions
+18 -13
View File
@@ -1,18 +1,23 @@
import { assetUrl } from "../../core/AssetUrls";
import { AllPlayers, Nukes } from "../../core/game/Game";
import { GameView, PlayerView } from "../../core/game/GameView";
import allianceIcon from "/images/AllianceIcon.svg?url";
import allianceIconFaded from "/images/AllianceIconFaded.svg?url";
import allianceRequestBlackIcon from "/images/AllianceRequestBlackIcon.svg?url";
import allianceRequestWhiteIcon from "/images/AllianceRequestWhiteIcon.svg?url";
import crownIcon from "/images/CrownIcon.svg?url";
import disconnectedIcon from "/images/DisconnectedIcon.svg?url";
import embargoBlackIcon from "/images/EmbargoBlackIcon.svg?url";
import embargoWhiteIcon from "/images/EmbargoWhiteIcon.svg?url";
import nukeRedIcon from "/images/NukeIconRed.svg?url";
import nukeWhiteIcon from "/images/NukeIconWhite.svg?url";
import questionMarkIcon from "/images/QuestionMarkIcon.svg?url";
import targetIcon from "/images/TargetIcon.svg?url";
import traitorIcon from "/images/TraitorIcon.svg?url";
const allianceIcon = assetUrl("images/AllianceIcon.svg");
const allianceIconFaded = assetUrl("images/AllianceIconFaded.svg");
const allianceRequestBlackIcon = assetUrl(
"images/AllianceRequestBlackIcon.svg",
);
const allianceRequestWhiteIcon = assetUrl(
"images/AllianceRequestWhiteIcon.svg",
);
const crownIcon = assetUrl("images/CrownIcon.svg");
const disconnectedIcon = assetUrl("images/DisconnectedIcon.svg");
const embargoBlackIcon = assetUrl("images/EmbargoBlackIcon.svg");
const embargoWhiteIcon = assetUrl("images/EmbargoWhiteIcon.svg");
const nukeRedIcon = assetUrl("images/NukeIconRed.svg");
const nukeWhiteIcon = assetUrl("images/NukeIconWhite.svg");
const questionMarkIcon = assetUrl("images/QuestionMarkIcon.svg");
const targetIcon = assetUrl("images/TargetIcon.svg");
const traitorIcon = assetUrl("images/TraitorIcon.svg");
export type PlayerIconId =
| "crown"
+11 -10
View File
@@ -1,17 +1,18 @@
import { Colord } from "colord";
import { assetUrl } from "../../core/AssetUrls";
import { Theme } from "../../core/configuration/Config";
import { TrainType, UnitType } from "../../core/game/Game";
import { UnitView } from "../../core/game/GameView";
import atomBombSprite from "/sprites/atombomb.png?url";
import hydrogenBombSprite from "/sprites/hydrogenbomb.png?url";
import mirvSprite from "/sprites/mirv2.png?url";
import samMissileSprite from "/sprites/samMissile.png?url";
import tradeShipSprite from "/sprites/tradeship.png?url";
import trainCarriageSprite from "/sprites/trainCarriage.png?url";
import trainLoadedCarriageSprite from "/sprites/trainCarriageLoaded.png?url";
import trainEngineSprite from "/sprites/trainEngine.png?url";
import transportShipSprite from "/sprites/transportship.png?url";
import warshipSprite from "/sprites/warship.png?url";
const atomBombSprite = assetUrl("sprites/atombomb.png");
const hydrogenBombSprite = assetUrl("sprites/hydrogenbomb.png");
const mirvSprite = assetUrl("sprites/mirv2.png");
const samMissileSprite = assetUrl("sprites/samMissile.png");
const tradeShipSprite = assetUrl("sprites/tradeship.png");
const trainCarriageSprite = assetUrl("sprites/trainCarriage.png");
const trainLoadedCarriageSprite = assetUrl("sprites/trainCarriageLoaded.png");
const trainEngineSprite = assetUrl("sprites/trainEngine.png");
const transportShipSprite = assetUrl("sprites/transportship.png");
const warshipSprite = assetUrl("sprites/warship.png");
// Can't reuse TrainType because "loaded" is not a type, just an attribute
const TrainTypeSprite = {
@@ -1,3 +1,4 @@
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import { Cell } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
@@ -6,8 +7,8 @@ import { AlternateViewEvent } from "../../InputHandler";
import { renderTroops } from "../../Utils";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import shieldIcon from "/images/ShieldIconWhite.svg?url";
import swordIcon from "/images/SwordIconWhite.svg?url";
const shieldIcon = assetUrl("images/ShieldIconWhite.svg");
const swordIcon = assetUrl("images/SwordIconWhite.svg");
export function troopAttackColor(
attackerTroops: number,
+3 -2
View File
@@ -1,5 +1,6 @@
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import { MessageType, PlayerType, UnitType } from "../../../core/game/Game";
import {
@@ -22,8 +23,8 @@ import {
GoToPositionEvent,
GoToUnitEvent,
} from "./Leaderboard";
import soldierIcon from "/images/SoldierIcon.svg?url";
import swordIcon from "/images/SwordIcon.svg?url";
const soldierIcon = assetUrl("images/SoldierIcon.svg");
const swordIcon = assetUrl("images/SwordIcon.svg");
@customElement("attacks-display")
export class AttacksDisplay extends LitElement implements Layer {
+12 -11
View File
@@ -1,6 +1,7 @@
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import {
BuildableUnit,
@@ -25,17 +26,17 @@ import { renderNumber } from "../../Utils";
import { TransformHandler } from "../TransformHandler";
import { UIState } from "../UIState";
import { Layer } from "./Layer";
import warshipIcon from "/images/BattleshipIconWhite.svg?url";
import cityIcon from "/images/CityIconWhite.svg?url";
import factoryIcon from "/images/FactoryIconWhite.svg?url";
import goldCoinIcon from "/images/GoldCoinIcon.svg?url";
import mirvIcon from "/images/MIRVIcon.svg?url";
import missileSiloIcon from "/images/MissileSiloIconWhite.svg?url";
import hydrogenBombIcon from "/images/MushroomCloudIconWhite.svg?url";
import atomBombIcon from "/images/NukeIconWhite.svg?url";
import portIcon from "/images/PortIcon.svg?url";
import samlauncherIcon from "/images/SamLauncherIconWhite.svg?url";
import shieldIcon from "/images/ShieldIconWhite.svg?url";
const warshipIcon = assetUrl("images/BattleshipIconWhite.svg");
const cityIcon = assetUrl("images/CityIconWhite.svg");
const factoryIcon = assetUrl("images/FactoryIconWhite.svg");
const goldCoinIcon = assetUrl("images/GoldCoinIcon.svg");
const mirvIcon = assetUrl("images/MIRVIcon.svg");
const missileSiloIcon = assetUrl("images/MissileSiloIconWhite.svg");
const hydrogenBombIcon = assetUrl("images/MushroomCloudIconWhite.svg");
const atomBombIcon = assetUrl("images/NukeIconWhite.svg");
const portIcon = assetUrl("images/PortIcon.svg");
const samlauncherIcon = assetUrl("images/SamLauncherIconWhite.svg");
const shieldIcon = assetUrl("images/ShieldIconWhite.svg");
export interface BuildItemDisplay {
unitType: PlayerBuildableUnitType;
+4 -3
View File
@@ -1,5 +1,6 @@
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import { Gold } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
@@ -8,9 +9,9 @@ import { AttackRatioEvent } from "../../InputHandler";
import { renderNumber, renderTroops } from "../../Utils";
import { UIState } from "../UIState";
import { Layer } from "./Layer";
import goldCoinIcon from "/images/GoldCoinIcon.svg?url";
import soldierIcon from "/images/SoldierIcon.svg?url";
import swordIcon from "/images/SwordIcon.svg?url";
const goldCoinIcon = assetUrl("images/GoldCoinIcon.svg");
const soldierIcon = assetUrl("images/SoldierIcon.svg");
const swordIcon = assetUrl("images/SwordIcon.svg");
@customElement("control-panel")
export class ControlPanel extends LitElement implements Layer {
+6 -5
View File
@@ -2,6 +2,7 @@ import { html, LitElement } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { DirectiveResult } from "lit/directive.js";
import { unsafeHTML, UnsafeHTMLDirective } from "lit/directives/unsafe-html.js";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import {
AllPlayers,
@@ -37,11 +38,11 @@ import { GoToPlayerEvent, GoToUnitEvent } from "./Leaderboard";
import { getMessageTypeClasses, translateText } from "../../Utils";
import { UIState } from "../UIState";
import allianceIcon from "/images/AllianceIconWhite.svg?url";
import chatIcon from "/images/ChatIconWhite.svg?url";
import donateGoldIcon from "/images/DonateGoldIconWhite.svg?url";
import nukeIcon from "/images/NukeIconWhite.svg?url";
import swordIcon from "/images/SwordIconWhite.svg?url";
const allianceIcon = assetUrl("images/AllianceIconWhite.svg");
const chatIcon = assetUrl("images/ChatIconWhite.svg");
const donateGoldIcon = assetUrl("images/DonateGoldIconWhite.svg");
const nukeIcon = assetUrl("images/NukeIconWhite.svg");
const swordIcon = assetUrl("images/SwordIconWhite.svg");
interface GameEvent {
description: string;
@@ -1,6 +1,7 @@
import { Colord } from "colord";
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import { GameMode, Team } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
@@ -9,10 +10,12 @@ import { getTranslatedPlayerTeamLabel, translateText } from "../../Utils";
import { ImmunityBarVisibleEvent } from "./ImmunityTimer";
import { Layer } from "./Layer";
import { SpawnBarVisibleEvent } from "./SpawnTimer";
import leaderboardRegularIcon from "/images/LeaderboardIconRegularWhite.svg?url";
import leaderboardSolidIcon from "/images/LeaderboardIconSolidWhite.svg?url";
import teamRegularIcon from "/images/TeamIconRegularWhite.svg?url";
import teamSolidIcon from "/images/TeamIconSolidWhite.svg?url";
const leaderboardRegularIcon = assetUrl(
"images/LeaderboardIconRegularWhite.svg",
);
const leaderboardSolidIcon = assetUrl("images/LeaderboardIconSolidWhite.svg");
const teamRegularIcon = assetUrl("images/TeamIconRegularWhite.svg");
const teamSolidIcon = assetUrl("images/TeamIconSolidWhite.svg");
@customElement("game-left-sidebar")
export class GameLeftSidebar extends LitElement implements Layer {
@@ -1,5 +1,6 @@
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import { GameType } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
@@ -12,11 +13,11 @@ import { Layer } from "./Layer";
import { ShowReplayPanelEvent } from "./ReplayPanel";
import { ShowSettingsModalEvent } from "./SettingsModal";
import { SpawnBarVisibleEvent } from "./SpawnTimer";
import exitIcon from "/images/ExitIconWhite.svg?url";
import FastForwardIconSolid from "/images/FastForwardIconSolidWhite.svg?url";
import pauseIcon from "/images/PauseIconWhite.svg?url";
import playIcon from "/images/PlayIconWhite.svg?url";
import settingsIcon from "/images/SettingIconWhite.svg?url";
const exitIcon = assetUrl("images/ExitIconWhite.svg");
const FastForwardIconSolid = assetUrl("images/FastForwardIconSolidWhite.svg");
const pauseIcon = assetUrl("images/PauseIconWhite.svg");
const playIcon = assetUrl("images/PlayIconWhite.svg");
const settingsIcon = assetUrl("images/SettingIconWhite.svg");
@customElement("game-right-sidebar")
export class GameRightSidebar extends LitElement implements Layer {
+3 -2
View File
@@ -1,5 +1,6 @@
import { LitElement } from "lit";
import { customElement } from "lit/decorators.js";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import { PlayerActions } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
@@ -19,8 +20,8 @@ import {
MenuElementParams,
rootMenuElement,
} from "./RadialMenuElements";
import donateTroopIcon from "/images/DonateTroopIconWhite.svg?url";
import swordIcon from "/images/SwordIconWhite.svg?url";
const donateTroopIcon = assetUrl("images/DonateTroopIconWhite.svg");
const swordIcon = assetUrl("images/SwordIconWhite.svg");
import { ContextMenuEvent } from "../../InputHandler";
+2 -1
View File
@@ -1,3 +1,4 @@
import { assetUrl } from "../../../core/AssetUrls";
import { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus";
import { PseudoRandom } from "../../../core/PseudoRandom";
@@ -229,7 +230,7 @@ export class NameLayer implements Layer {
} else if (flag !== undefined && flag !== null) {
const flagImg = document.createElement("img");
applyFlagStyles(flagImg);
flagImg.src = "/flags/" + flag + ".svg";
flagImg.src = assetUrl(`flags/${flag}.svg`);
nameDiv.appendChild(flagImg);
}
}
+11 -10
View File
@@ -1,6 +1,7 @@
import { LitElement, TemplateResult, html } from "lit";
import { ref } from "lit-html/directives/ref.js";
import { customElement, property, state } from "lit/decorators.js";
import { assetUrl } from "../../../core/AssetUrls";
import { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus";
import {
@@ -31,15 +32,15 @@ import { ImmunityBarVisibleEvent } from "./ImmunityTimer";
import { Layer } from "./Layer";
import { CloseRadialMenuEvent } from "./RadialMenu";
import { SpawnBarVisibleEvent } from "./SpawnTimer";
import allianceIcon from "/images/AllianceIcon.svg?url";
import warshipIcon from "/images/BattleshipIconWhite.svg?url";
import cityIcon from "/images/CityIconWhite.svg?url";
import factoryIcon from "/images/FactoryIconWhite.svg?url";
import goldCoinIcon from "/images/GoldCoinIcon.svg?url";
import missileSiloIcon from "/images/MissileSiloIconWhite.svg?url";
import portIcon from "/images/PortIcon.svg?url";
import samLauncherIcon from "/images/SamLauncherIconWhite.svg?url";
import soldierIcon from "/images/SoldierIcon.svg?url";
const allianceIcon = assetUrl("images/AllianceIcon.svg");
const warshipIcon = assetUrl("images/BattleshipIconWhite.svg");
const cityIcon = assetUrl("images/CityIconWhite.svg");
const factoryIcon = assetUrl("images/FactoryIconWhite.svg");
const goldCoinIcon = assetUrl("images/GoldCoinIcon.svg");
const missileSiloIcon = assetUrl("images/MissileSiloIconWhite.svg");
const portIcon = assetUrl("images/PortIcon.svg");
const samLauncherIcon = assetUrl("images/SamLauncherIconWhite.svg");
const soldierIcon = assetUrl("images/SoldierIcon.svg");
function euclideanDistWorld(
coord: { x: number; y: number },
@@ -377,7 +378,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
></div>`
: html`<img
class="h-6 aspect-3/4"
src=${"/flags/" + player.cosmetics.flag! + ".svg"}
src=${assetUrl(`flags/${player.cosmetics.flag!}.svg`)}
/>`
: html``}
<span>${player.displayName()}</span>
@@ -1,13 +1,14 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import { PlayerType } from "../../../core/game/Game";
import { PlayerView } from "../../../core/game/GameView";
import { actionButton } from "../../components/ui/ActionButton";
import { SendKickPlayerIntentEvent } from "../../Transport";
import { translateText } from "../../Utils";
import kickIcon from "/images/ExitIconWhite.svg?url";
import shieldIcon from "/images/ShieldIconWhite.svg?url";
const kickIcon = assetUrl("images/ExitIconWhite.svg");
const shieldIcon = assetUrl("images/ShieldIconWhite.svg");
@customElement("player-moderation-modal")
export class PlayerModerationModal extends LitElement {
+13 -12
View File
@@ -1,6 +1,7 @@
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import Countries from "resources/countries.json" with { type: "json" };
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import {
AllPlayers,
@@ -39,17 +40,17 @@ import { EmojiTable } from "./EmojiTable";
import { Layer } from "./Layer";
import "./PlayerModerationModal";
import "./SendResourceModal";
import allianceIcon from "/images/AllianceIconWhite.svg?url";
import chatIcon from "/images/ChatIconWhite.svg?url";
import donateGoldIcon from "/images/DonateGoldIconWhite.svg?url";
import donateTroopIcon from "/images/DonateTroopIconWhite.svg?url";
import emojiIcon from "/images/EmojiIconWhite.svg?url";
import shieldIcon from "/images/ShieldIconWhite.svg?url";
import stopTradingIcon from "/images/StopIconWhite.png?url";
import targetIcon from "/images/TargetIconWhite.svg?url";
import startTradingIcon from "/images/TradingIconWhite.png?url";
import traitorIcon from "/images/TraitorIconLightRed.svg?url";
import breakAllianceIcon from "/images/TraitorIconWhite.svg?url";
const allianceIcon = assetUrl("images/AllianceIconWhite.svg");
const chatIcon = assetUrl("images/ChatIconWhite.svg");
const donateGoldIcon = assetUrl("images/DonateGoldIconWhite.svg");
const donateTroopIcon = assetUrl("images/DonateTroopIconWhite.svg");
const emojiIcon = assetUrl("images/EmojiIconWhite.svg");
const shieldIcon = assetUrl("images/ShieldIconWhite.svg");
const stopTradingIcon = assetUrl("images/StopIconWhite.png");
const targetIcon = assetUrl("images/TargetIconWhite.svg");
const startTradingIcon = assetUrl("images/TradingIconWhite.png");
const traitorIcon = assetUrl("images/TraitorIconLightRed.svg");
const breakAllianceIcon = assetUrl("images/TraitorIconWhite.svg");
@customElement("player-panel")
export class PlayerPanel extends LitElement implements Layer {
@@ -493,7 +494,7 @@ export class PlayerPanel extends LitElement implements Layer {
<div class="flex items-center gap-2.5 flex-wrap">
${country && typeof flagCode === "string"
? html`<img
src="/flags/${encodeURIComponent(flagCode)}.svg"
src=${assetUrl(`flags/${encodeURIComponent(flagCode)}.svg`)}
alt=${country?.name ?? "Flag"}
class="h-10 w-10 rounded-full object-cover"
@error=${(e: Event) => {
+2 -1
View File
@@ -1,4 +1,5 @@
import * as d3 from "d3";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus, GameEvent } from "../../../core/EventBus";
import { CloseViewEvent } from "../../InputHandler";
import { getSvgAspectRatio, translateText } from "../../Utils";
@@ -9,7 +10,7 @@ import {
MenuElementParams,
TooltipKey,
} from "./RadialMenuElements";
import backIcon from "/images/BackIconWhite.svg?url";
const backIcon = assetUrl("images/BackIconWhite.svg");
function resolveColor(
item: MenuElement,
@@ -1,3 +1,4 @@
import { assetUrl } from "../../../core/AssetUrls";
import { Config } from "../../../core/configuration/Config";
import {
AllPlayers,
@@ -20,18 +21,18 @@ import { PlayerPanel } from "./PlayerPanel";
import { TooltipItem } from "./RadialMenu";
import { EventBus } from "../../../core/EventBus";
import allianceIcon from "/images/AllianceIconWhite.svg?url";
import boatIcon from "/images/BoatIconWhite.svg?url";
import buildIcon from "/images/BuildIconWhite.svg?url";
import chatIcon from "/images/ChatIconWhite.svg?url";
import donateGoldIcon from "/images/DonateGoldIconWhite.svg?url";
import donateTroopIcon from "/images/DonateTroopIconWhite.svg?url";
import emojiIcon from "/images/EmojiIconWhite.svg?url";
import infoIcon from "/images/InfoIcon.svg?url";
import swordIcon from "/images/SwordIconWhite.svg?url";
import targetIcon from "/images/TargetIconWhite.svg?url";
import traitorIcon from "/images/TraitorIconWhite.svg?url";
import xIcon from "/images/XIcon.svg?url";
const allianceIcon = assetUrl("images/AllianceIconWhite.svg");
const boatIcon = assetUrl("images/BoatIconWhite.svg");
const buildIcon = assetUrl("images/BuildIconWhite.svg");
const chatIcon = assetUrl("images/ChatIconWhite.svg");
const donateGoldIcon = assetUrl("images/DonateGoldIconWhite.svg");
const donateTroopIcon = assetUrl("images/DonateTroopIconWhite.svg");
const emojiIcon = assetUrl("images/EmojiIconWhite.svg");
const infoIcon = assetUrl("images/InfoIcon.svg");
const swordIcon = assetUrl("images/SwordIconWhite.svg");
const targetIcon = assetUrl("images/TargetIconWhite.svg");
const traitorIcon = assetUrl("images/TraitorIconWhite.svg");
const xIcon = assetUrl("images/XIcon.svg");
export interface MenuElementParams {
myPlayer: PlayerView;
+14 -13
View File
@@ -2,25 +2,26 @@ import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { crazyGamesSDK } from "src/client/CrazyGamesSDK";
import { PauseGameIntentEvent } from "src/client/Transport";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import { UserSettings } from "../../../core/game/UserSettings";
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
import { translateText } from "../../Utils";
import SoundManager from "../../sound/SoundManager";
import { Layer } from "./Layer";
import structureIcon from "/images/CityIconWhite.svg?url";
import cursorPriceIcon from "/images/CursorPriceIconWhite.svg?url";
import darkModeIcon from "/images/DarkModeIconWhite.svg?url";
import emojiIcon from "/images/EmojiIconWhite.svg?url";
import exitIcon from "/images/ExitIconWhite.svg?url";
import explosionIcon from "/images/ExplosionIconWhite.svg?url";
import mouseIcon from "/images/MouseIconWhite.svg?url";
import ninjaIcon from "/images/NinjaIconWhite.svg?url";
import settingsIcon from "/images/SettingIconWhite.svg?url";
import sirenIcon from "/images/SirenIconWhite.svg?url";
import swordIcon from "/images/SwordIconWhite.svg?url";
import treeIcon from "/images/TreeIconWhite.svg?url";
import musicIcon from "/images/music.svg?url";
const structureIcon = assetUrl("images/CityIconWhite.svg");
const cursorPriceIcon = assetUrl("images/CursorPriceIconWhite.svg");
const darkModeIcon = assetUrl("images/DarkModeIconWhite.svg");
const emojiIcon = assetUrl("images/EmojiIconWhite.svg");
const exitIcon = assetUrl("images/ExitIconWhite.svg");
const explosionIcon = assetUrl("images/ExplosionIconWhite.svg");
const mouseIcon = assetUrl("images/MouseIconWhite.svg");
const ninjaIcon = assetUrl("images/NinjaIconWhite.svg");
const settingsIcon = assetUrl("images/SettingIconWhite.svg");
const sirenIcon = assetUrl("images/SirenIconWhite.svg");
const swordIcon = assetUrl("images/SwordIconWhite.svg");
const treeIcon = assetUrl("images/TreeIconWhite.svg");
const musicIcon = assetUrl("images/music.svg");
export class ShowSettingsModalEvent {
constructor(
@@ -1,4 +1,5 @@
import * as PIXI from "pixi.js";
import { assetUrl } from "../../../core/AssetUrls";
import { Theme } from "../../../core/configuration/Config";
import {
Cell,
@@ -7,12 +8,12 @@ import {
} from "../../../core/game/Game";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler";
import anchorIcon from "/images/AnchorIcon.png?url";
import cityIcon from "/images/CityIcon.png?url";
import factoryIcon from "/images/FactoryUnit.png?url";
import missileSiloIcon from "/images/MissileSiloUnit.png?url";
import SAMMissileIcon from "/images/SamLauncherUnit.png?url";
import shieldIcon from "/images/ShieldIcon.png?url";
const anchorIcon = assetUrl("images/AnchorIcon.png");
const cityIcon = assetUrl("images/CityIcon.png");
const factoryIcon = assetUrl("images/FactoryUnit.png");
const missileSiloIcon = assetUrl("images/MissileSiloUnit.png");
const SAMMissileIcon = assetUrl("images/SamLauncherUnit.png");
const shieldIcon = assetUrl("images/ShieldIcon.png");
export const STRUCTURE_SHAPES: Partial<Record<UnitType, ShapeType>> = {
[UnitType.City]: "circle",
@@ -2,6 +2,7 @@ import { extend } from "colord";
import a11yPlugin from "colord/plugins/a11y";
import { OutlineFilter } from "pixi-filters";
import * as PIXI from "pixi.js";
import { assetUrl } from "../../../core/AssetUrls";
import { Theme } from "../../../core/configuration/Config";
import { EventBus } from "../../../core/EventBus";
import { wouldNukeBreakAlliance } from "../../../core/execution/Util";
@@ -42,7 +43,7 @@ import {
STRUCTURE_SHAPES,
ZOOM_THRESHOLD,
} from "./StructureDrawingUtils";
import bitmapFont from "/fonts/round_6x6_modified.xml?url";
const bitmapFont = assetUrl("fonts/round_6x6_modified.xml");
/** True for nuke types (AtomBomb, HydrogenBomb): ghost is preserved after placement so user can place multiple or keep selection (Enter/key confirm). */
export function shouldPreserveGhostAfterBuild(unitType: UnitType): boolean {
+7 -6
View File
@@ -1,4 +1,5 @@
import { colord, Colord } from "colord";
import { assetUrl } from "../../../core/AssetUrls";
import { Theme } from "../../../core/configuration/Config";
import { EventBus } from "../../../core/EventBus";
import { TransformHandler } from "../TransformHandler";
@@ -8,12 +9,12 @@ import { Cell, UnitType } from "../../../core/game/Game";
import { euclDistFN, isometricDistFN } from "../../../core/game/GameMap";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, UnitView } from "../../../core/game/GameView";
import cityIcon from "/images/buildings/cityAlt1.png?url";
import factoryIcon from "/images/buildings/factoryAlt1.png?url";
import shieldIcon from "/images/buildings/fortAlt3.png?url";
import anchorIcon from "/images/buildings/port1.png?url";
import missileSiloIcon from "/images/buildings/silo1.png?url";
import SAMMissileIcon from "/images/buildings/silo4.png?url";
const cityIcon = assetUrl("images/buildings/cityAlt1.png");
const factoryIcon = assetUrl("images/buildings/factoryAlt1.png");
const shieldIcon = assetUrl("images/buildings/fortAlt3.png");
const anchorIcon = assetUrl("images/buildings/port1.png");
const missileSiloIcon = assetUrl("images/buildings/silo1.png");
const SAMMissileIcon = assetUrl("images/buildings/silo4.png");
const underConstructionColor = colord("rgb(150,150,150)");
+12 -11
View File
@@ -1,5 +1,6 @@
import { html, LitElement } from "lit";
import { customElement } from "lit/decorators.js";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import {
BuildableUnit,
@@ -16,17 +17,17 @@ import {
import { renderNumber, translateText } from "../../Utils";
import { UIState } from "../UIState";
import { Layer } from "./Layer";
import warshipIcon from "/images/BattleshipIconWhite.svg?url";
import cityIcon from "/images/CityIconWhite.svg?url";
import factoryIcon from "/images/FactoryIconWhite.svg?url";
import goldCoinIcon from "/images/GoldCoinIcon.svg?url";
import mirvIcon from "/images/MIRVIcon.svg?url";
import missileSiloIcon from "/images/MissileSiloIconWhite.svg?url";
import hydrogenBombIcon from "/images/MushroomCloudIconWhite.svg?url";
import atomBombIcon from "/images/NukeIconWhite.svg?url";
import portIcon from "/images/PortIcon.svg?url";
import samLauncherIcon from "/images/SamLauncherIconWhite.svg?url";
import defensePostIcon from "/images/ShieldIconWhite.svg?url";
const warshipIcon = assetUrl("images/BattleshipIconWhite.svg");
const cityIcon = assetUrl("images/CityIconWhite.svg");
const factoryIcon = assetUrl("images/FactoryIconWhite.svg");
const goldCoinIcon = assetUrl("images/GoldCoinIcon.svg");
const mirvIcon = assetUrl("images/MIRVIcon.svg");
const missileSiloIcon = assetUrl("images/MissileSiloIconWhite.svg");
const hydrogenBombIcon = assetUrl("images/MushroomCloudIconWhite.svg");
const atomBombIcon = assetUrl("images/NukeIconWhite.svg");
const portIcon = assetUrl("images/PortIcon.svg");
const samLauncherIcon = assetUrl("images/SamLauncherIconWhite.svg");
const defensePostIcon = assetUrl("images/ShieldIconWhite.svg");
@customElement("unit-display")
export class UnitDisplay extends LitElement implements Layer {