support for unlockable flags (#3479)

## Description:

Add support for purchasable/gated flags.

* Create a new "Store" modal that renders both skins & flags
* move all store related logic out of TerritoryPatternsModal
* use nation:code for existing nation flags & flag:key for gated flags
* check if user has the appropriate flags before purchasing

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] 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:

evan
This commit is contained in:
Evan
2026-03-23 17:09:18 -07:00
committed by GitHub
parent 426806299f
commit 39ad547c04
30 changed files with 1144 additions and 1155 deletions
+5 -14
View File
@@ -1,4 +1,3 @@
import { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus";
import { PseudoRandom } from "../../../core/PseudoRandom";
import { Theme } from "../../../core/configuration/Config";
@@ -210,22 +209,14 @@ export class NameLayer implements Layer {
element.classList.add("player-flag");
element.style.opacity = "0.8";
element.style.zIndex = "1";
element.style.aspectRatio = "3/4";
element.style.objectFit = "contain";
};
if (player.cosmetics.flag) {
const flag = player.cosmetics.flag;
if (flag !== undefined && flag !== null && flag.startsWith("!")) {
const flagWrapper = document.createElement("div");
applyFlagStyles(flagWrapper);
renderPlayerFlag(flag, flagWrapper);
nameDiv.appendChild(flagWrapper);
} else if (flag !== undefined && flag !== null) {
const flagImg = document.createElement("img");
applyFlagStyles(flagImg);
flagImg.src = "/flags/" + flag + ".svg";
nameDiv.appendChild(flagImg);
}
const flagImg = document.createElement("img");
applyFlagStyles(flagImg);
flagImg.src = player.cosmetics.flag;
nameDiv.appendChild(flagImg);
}
nameDiv.classList.add("player-name");
nameDiv.style.color = this.theme.textColor(player);
@@ -476,14 +476,8 @@ export class PerformanceOverlay extends LitElement implements Layer {
this.updateTickMetrics(event.tickExecutionDuration, event.tickDelay);
};
private onUserSettingsChanged = (event: Event) => {
const customEvent = event as CustomEvent<{
key?: string;
value?: unknown;
}>;
if (customEvent.detail?.key !== "settings.performanceOverlay") return;
const nextVisible = customEvent.detail.value === true;
private onUserSettingsChanged = (event: CustomEvent) => {
const nextVisible = (event.detail as boolean) === true;
if (this.isVisible === nextVisible) return;
this.setVisible(nextVisible);
};
@@ -511,7 +505,7 @@ export class PerformanceOverlay extends LitElement implements Layer {
if (!this.isUserSettingsListenerAttached) {
globalThis.addEventListener(
"user-settings-changed",
"event:user-settings-changed:settings.performanceOverlay",
this.onUserSettingsChanged,
);
this.isUserSettingsListenerAttached = true;
@@ -523,7 +517,7 @@ export class PerformanceOverlay extends LitElement implements Layer {
if (this.isUserSettingsListenerAttached) {
globalThis.removeEventListener(
"user-settings-changed",
"event:user-settings-changed:settings.performanceOverlay",
this.onUserSettingsChanged,
);
this.isUserSettingsListenerAttached = false;
@@ -1,7 +1,5 @@
import { LitElement, TemplateResult, html } from "lit";
import { ref } from "lit-html/directives/ref.js";
import { customElement, property, state } from "lit/decorators.js";
import { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus";
import {
PlayerProfile,
@@ -364,21 +362,10 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
)}"
>
${player.cosmetics.flag
? player.cosmetics.flag!.startsWith("!")
? html`<div
class="h-6 aspect-3/4 player-flag"
${ref((el) => {
if (el instanceof HTMLElement) {
requestAnimationFrame(() => {
renderPlayerFlag(player.cosmetics.flag!, el);
});
}
})}
></div>`
: html`<img
class="h-6 aspect-3/4"
src=${"/flags/" + player.cosmetics.flag! + ".svg"}
/>`
? html`<img
class="h-6 object-contain"
src=${player.cosmetics.flag!}
/>`
: html``}
<span>${player.name()}</span>
${playerTeam !== "" && player.type() !== PlayerType.Bot
+1 -1
View File
@@ -203,7 +203,7 @@ export class WinModal extends LitElement implements Layer {
.requiresPurchase=${true}
.onSelect=${(p: Pattern | null) => {}}
.onPurchase=${(p: Pattern, colorPalette: ColorPalette | null) =>
handlePurchase(p, colorPalette)}
handlePurchase(p.product!, colorPalette?.name)}
></pattern-button>
`,
)}