mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-13 18:48:57 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)");
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user